条件变量似乎是学习线程同步以来最难的东西。不过,慢慢来,总是能够学会的,多读多看多查资料。
1. 实现方法
接下来,给出 pthread_cond_wait 和 pthread_cond_signal 的伪代码(参考man pthread_cond_signal)。语句后面的编号代表时间上的执行顺序。
pthread_cond_wait(mutex, cond) {
value = cond->value; /* 1 */
pthread_mutex_unlock(mutex); /* 2 */
pthread_mutex_lock(cond->mutex); /* 10 */
if (value == cond->value) { /* 11 */
me->next_cond = cond->waiter;
cond->waiter = me;
pthread_mutex_unlock(cond->mutex);
unable_to_run(me);
}
else {
pthread_mutex_unlock(cond->mutex); /* 12 */
}
pthread_mutex_lock(mutex); /* 13 */
}
pthread_cond_signal(cond) {
pthread_mutex_lock(cond->mutex); /* 3 */
cond->value++; /* 4 */
if (cond->waiter) { /* 5 */
sleeper = cond->waiter; /* 6 */
cond->waiter = sleeper->next_cond; /* 7 */
able_to_run(sleeper); /* 8 */
}
pthread_mutex_unlock(cond->mutex); /* 9 */
}
对于一般情况,wait 完成进入等待队列的情况比较简单,就不考虑了。我们考虑在释放互斥锁后,进入 wait 等待队列前的情况:
假设只有两个线程 A 和 B
线程 A 执行完语句 2 后正在尝试进入等待队列(已经解锁但是尚未阻塞),即期望执行语句 10 以及后面的进入等待队列. 同时线程 B 正在执行 pthread_cond_signal 中的语句 3 到语句 9. 此时等待队列为空,所以 if (cond->waiter) 条件不成立。
另外条件也发生了改变(cond->value++
),线程 B 完成了 pthread_signal 返回后,线程 A 执行语句 10,因为条件已经成立,所以线程 A 不会进入等待队列直接返回。宏观上看起来,好像就是从“等待队列”中被唤醒一样。
如果你还记得在上一篇条件变量中的叙述:
pthread_cond_wait 被分解成了三步,其中 a1 和 a2 是一次执行完的。这两个步骤是原子的。
而实际上,pthread_cond_wait 的实现应该像本文上面的伪代码, 即阻塞不是必须的,能给人一种错觉就够了。
再次回顾一下前文所述:
如果线程 A 在释放锁后(语句 a1),执行语句 a2 的即将要阻塞的时候,线程 B 此时调用 pthread_cond_signal 或者 pthread_cond_broadcast ,感觉就好像即将要被阻塞的线程 A 已经阻塞过一样。
现在你应该理解了所谓的“原子”是怎么做的了吧。
2. 虚假唤醒
英文术语叫 spurious wakeup。
在前一篇文章中说,pthread_cond_signal 可以唤醒队列中的一个线程,而实际上,它也可能唤醒不止一个线程。
man 手册做此解释::
在多核系统中,要避免 pthread_cond_signal 唤醒超过一个以上的线程,似乎是不可能的。
继续使用前面使用的伪代码。
假设只有三个线程 A 、B 和 C
线程 A 执行完语句 2 后正在尝试进入等待队列(已经解锁但是尚未阻塞),即期望执行语句 10 以及后面的进入等待队列. 同时线程 B 正在执行 pthread_cond_signal 中的语句 3 到语句 9. 另一方面,线程 C 正处于等待队列中。
和第 1 节中不一样的是,语句 5 if(cond->waiter)
成立,因为队列中有线程 C。因此 pthread_cond_signal 会唤醒线程 C。而线程 A 也会因为if(value == cond->value)
直接执行语句13. 记住,此时的 A 是唤醒状态,不是位于等待队列中。
一旦线程 C 释放锁后,线程 A 就返回,然而此时,条件可能已经不成立(比如条件被程 C 更改),故出现虚假唤醒的状态。
3. 解决方案
实际上在上一篇的实验中已经给出了解决方案,即即使 pthread_cond_wait 已经返回,也不意味着条件一定成立,代码中使用了循环 while(finished == 1)
反复测试。
pthread_mutex_lock(&lock);
while(finished == 0) {
pthread_cond_wait(&cond, &lock);
}
pthread_mutex_unlock(&lock);
切记,这里不要使用 if
,而是 while
!!! 目的在于防止虚假唤醒——spurious wakeup.
4. 总结
- 深入理解条件变量的实现
- 什么是虚拟唤醒
- 如何避免虚假唤醒