87-深入条件变量

条件变量似乎是学习线程同步以来最难的东西。不过,慢慢来,总是能够学会的,多读多看多查资料。

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_signal 已经返回,也不意味着条件一定成立,代码中使用了循环 while(finished == 1) 反复测试。

pthread_mutex_lock(&lock);
while(finished == 0) {
  pthread_cond_wait(&cond, &lock);
}
pthread_mutex_unlock(&lock);

切记,这里不要使用 if,而是 while!!! 目的在于防止虚假唤醒——spurious wakeup.

4. 总结

  • 深入理解条件变量的实现
  • 什么是虚拟唤醒
  • 如何避免虚假唤醒

你可能感兴趣的:(linux,编程学习笔记,Linux,环境编程修炼指南-外功心法)