10.2.2 自旋互斥体
还有一种特殊的基于轮询的进程内互斥体,这通常是一种糟糕的实践。简单地说,轮询就是通过反复不断地测试来等待某个条件的改变,像这样:
- int g_flag;
-
- // 等待线程
- while(0 == g_flag)
- {}
- . . . // 现在做我们想做的事吧
这种东西会吞噬处理器时钟周期,因为跟那个改变标志变量从而允许轮询线程继续执行的线程相比,轮询线程通常2被给予同样的优先级。轮询是个糟糕的主意,通常标志着使用它的人是个刚接触多线程的菜鸟,或者等着拿解雇费的萎瓜。
然而,某些场合则非常合适于采用自旋(spinning)。让我们看看下面自旋互斥体的实现,这是UNIXSTL3中的spin_mutex类(如程序清单10.2):
程序清单10.2
- class spin_mutex
- {
- public:
- explicit spin_mutex(sint32_t *p = NULL)
- : m_spinCount((NULL != p) p : &m_internalCount)
- , m_internalCount(0)
- {}
- void lock()
- {
- for(; 0 != atomic_write(m_spinCount, 1); sched_yield())
- {}
- }
- void unlock()
- {
- atomic_set(m_spinCount, 0);
- }
- // 成员
- private:
- sint32_t *m_spinCount;
- sint32_t m_internalCount;
- // 声明但不予实现
- private:
- spin_mutex(class_type const &rhs);
- spin_mutex &operator =(class_type const &rhs);
- };
自旋互斥体的工作机制相当简单。当lock()被调用时,执行一次原子写操作,将自旋变量*m_spinCount(整型)置为1。如果它原先的值为0,就意味着调用者是设置它的第一个线程,从而"获取"该互斥体,然后该方法(lock())返回。但如果原先的值为1,那就意味着调用者被另一个线程拒之门外1,无法获得该互斥体,于是接着调用PTHREAD函数sched_yield(),将执行机会让给其他线程。以后当它被再次唤醒时,它就会再次尝试获取该互斥体。这个过程不断重复,直到获取成功,从而该互斥体的所有权就得到了锁定。
当获取了互斥体的线程调用unlock()时,自旋变量*m_spinCount会被重置为0,从而允许其他线程再次获取该互斥体。由于该类可以基于一个外部的自旋变量来创建,因而引入了成员m_internalCount,使得构造函数看上去有一点复杂,不过这在某些特定的场合可能会非常有用(我们将在第11章和第31章中看到这一点)。
然而,当出现激烈的竞争情况时,自旋互斥体就不再是好的解决方案了。不过,在出现竞争的可能性很低并且获取/释放同步机制的开销也不高的情况下,使用自旋互斥体是行之有效的方案。由于它们可能导致高昂的开销,所以我倾向于仅将它们用在初始化动作中,这种情况下竞争极其罕见,但理论上仍是可能的,你心里要有数。