用条件变量实现事件等待器的正确与错误做法 提到了8种 基于 linux pthread 条件变量实现的 Waiter classes,并分析了几种错误实现的错误之处。本文进一步分析一下几种正确实现的程序行为,加深对Linux pthread 条件变量的理解。
下面给出一个可以用于single waiter的WaiterClass的正确实现。
class Waiter : private WaiterBase
{
public:
void wait()
{
CHECK_SUCCESS(pthread_mutex_lock(&mutex_));
while (!signaled_)
{
CHECK_SUCCESS(pthread_cond_wait(&cond_, &mutex_));
}
CHECK_SUCCESS(pthread_mutex_unlock(&mutex_));
}
void signal()
{
CHECK_SUCCESS(pthread_mutex_lock(&mutex_)); // 0
signaled_ = true; // 1
CHECK_SUCCESS(pthread_mutex_unlock(&mutex_)); // 2
CHECK_SUCCESS(pthread_cond_signal(&cond_)); // 3
}
private:
bool signaled_ = false;
};
要实现一个正确的Waiter Class, 由 1 可知 wait() 函数的指令顺序必须如上所示。但signal() 函数可以有几种不同的实现,即代码行 1,2,3可以有几种不同的顺序组合,使得Waiter Class是正确的。这几种组合包括了1 给出的几种正确实现。
首先对 代码行1,2,3 给出所有可能的排列形式,然后一一说明其是否正确。
a. 1-2-3
b. 1-3-2
c. 2-1-3
d. 2-3-1
e. 3-1-2
f. 3-2-1
要分析以上6种实现的对错,先要了解一下 pthread_cond_wait 和 pthread_cond_signal 的内部流程。
假设等待信号的线程为A,发出信号的线程为B。
线程A,pthread_cond_wait()内部包括以下流程:
- 将 waiter 加入cond 的 _wseq 队列 (分为G1 G2两个组)
- 释放mutex
- 自旋等待,检查 __g_signals,自旋次数结束,进入 futex_wait,休眠
线程B,pthread_cond_signal()包括以下流程:
- 检查 cond __wseq,若没有等待者则直接返回。
- 有等待者,检查是否需要切换组(例如首次调用 wait 后 G1 为空,G2有一个等待者,则首次调用 signal 后需要将 G2 切换为 G1),递增 __g_signals,递减 __g_size(未唤醒的 waiters 个数),再调用 futex_wake。
signal 将一直唤醒G1组的 waiters 直到 G1 所有的 waiters 都被唤醒。若此过程种有新到达的waiter, 则存入G2 组(之后到达的 waiter 也存入G2)。G2组会在下次signal 调用时转为G1组,因此signal永远只唤醒G1 组的 waiters。(参考)
有了以上的细节,可以很容易的得出问题的结论。
- 首先应当排除e和f。ef拿到锁又马上释放锁,1和2都不被锁保护,1,2执行的时机可以任意穿插到wait()函数各语句zhi'jian,从而语句2发出的信号很可能会丢失
- d也是错误的。一种导致线程A饿死的时序:2发信号,唤醒线程A,3释放锁,A获得锁,判断布尔值,条件为真,再次进入等待,一直休眠
- ac是正确的。不管发信号时是否修改了布尔值,被唤醒的A线程始终无法得到mutex,直到3释放锁,futex_wake通知线程A, A才可以拿到锁,从pthread_cond_wait返回,继续向下执行,此时布尔值已经改变,条件测试为假,跳出循环,继续执行。事实上,由于编译器乱序和CPU 乱序,ac有可能运行的次序是相同的。
- b也是正确的。B线程在修改布尔变量后释放锁,此时线程A依然处在休眠状态,不知道发生的一切,只有当B发信号唤醒A时,A才可以看到这一切,成功获得锁,进而继续向下执行。
总结发现,只要布尔值的修改是在释放锁的操作之前,就能保证其正确性。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。