为什么pthread_cond_wait会出现虚假唤醒?

180

引用手册:

当使用条件变量时,每个条件等待都会涉及与共享变量相关联的布尔谓词,如果线程应该继续,则该谓词为真。可能发生从pthread_cond_timedwait()或pthread_cond_wait()函数的虚假唤醒。由于从pthread_cond_timedwait()或pthread_cond_wait()返回并不意味着该谓词的值,因此应在此类返回后重新评估该谓词。

所以,即使您没有发出信号,pthread_cond_wait也可能会返回。乍一看,这似乎相当可怕。这就像一个随机返回错误值或在实际到达适当返回语句之前随机返回的函数。它似乎是一个重大的漏洞。但是,他们选择在手册中记录这一点而不是修复它似乎表明pthread_cond_wait最终会无意地唤醒的原因是合法的。大概有关于它如何工作的内在因素使得它不能被避免。问题是什么。

为什么pthread_cond_wait会出现虚假唤醒?为什么它不能保证只有在被正确通知时才会被唤醒?有人能解释一下它的虚假行为的原因吗?


6
我想这可能与进程捕获信号后返回有关。大多数*nix系统在信号中断了阻塞调用后不会重新启动它,而是设置/返回一个错误代码表示发生了信号。 - cHao
1
@cHao:需要注意的是,由于条件变量存在其他导致虚假唤醒的原因,处理信号对于pthread_cond_(timed)wait来说并不是一个错误:“如果传递了信号...线程将恢复等待条件变量的状态,就好像没有被中断一样,或者它将返回由于虚假唤醒而导致的零值”。其他阻塞函数在被信号中断时会指示EINTR(例如read),或者需要恢复(例如pthread_mutex_lock)。因此,如果没有其他虚假唤醒的原因,pthread_cond_wait可以像这些函数之一那样定义。 - Steve Jessop
5
维基百科上的相关文章:虚假唤醒 - Palec
5
有用的Vladimir Prus: 虚假唤醒 - iammilind
许多函数无法完全完成它们的工作(中断I/O),观察函数可能会收到非事件,例如更改到已取消或恢复的目录。问题出在哪里? - curiousguy
6个回答

142

“虚假唤醒”至少有两个含义:

  • pthread_cond_wait 中阻塞的线程可能会从调用中返回,即使条件没有发生 pthread_cond_signalpthread_cond_broadcast 的调用。
  • pthread_cond_wait 中阻塞的线程由于对条件发出了 pthread_cond_signalpthread_cond_broadcast 的调用而返回,但重新获取互斥锁后发现底层谓词不再为真。

但即使条件变量实现不允许前一种情况,后一种情况仍然可能发生。考虑一个生产者消费者队列和三个线程。

  • 线程1刚刚取出了一个元素并释放了互斥锁,队列现在为空。该线程正在某个CPU上处理所获取的元素。
  • 线程2尝试取出一个元素,但在互斥锁下检查到队列为空时,调用 pthread_cond_wait 并在调用中阻塞等待信号/广播。
  • 线程3获得互斥锁,向队列插入一个新元素,通知条件变量并释放锁。
  • 响应于线程3的通知,等待条件的线程2被调度运行。
  • 但是,在线程2成功获取CPU和队列锁之前,线程1完成了当前任务并返回到队列中进行更多工作。它获得队列锁,检查谓词并发现队列中有工作。然后继续出队线程3插入的元素,释放锁,并对线程3排队的元素执行其它操作。
  • 现在线程2获得CPU并获取锁,但在检查谓词时发现队列为空。线程1“窃取”了该元素,因此唤醒似乎是虚假的。线程2需要再次等待条件。

因此,由于您已经总是需要在循环下检查谓词,所以底层条件变量可能会有其他类型的虚假唤醒并没有什么区别。


33
基本上,当事件被用作计数器的同步机制时,就会发生这种情况。可遗憾的是,POSIX信号量(至少在Linux上)也会受到虚假唤醒的影响。我觉得有点奇怪的是,同步原语的一个基本功能缺陷被接受为“正常”,并且必须在用户级别解决 :( 假设如果一个系统调用有一个“虚假段错误”或者“虚假连接到错误的URL”或者“虚假打开错误的文件”的部分,开发人员会感到非常愤怒。 - Martin James
6
“虚假唤醒”是指调用pthread_cond_broadcast()后最常见的情况。比如你有一个由5个线程组成的线程池,两个被广播唤醒并完成了工作。另外三个被唤醒时发现工作已经完成了。在多处理器系统中也可能发生条件信号意外唤醒多个线程的情况。代码只需再次检查谓词,看到无效状态就会继续休眠。无论哪种情况,检查谓词都能解决问题。总的来说,我认为用户不应该使用原始的POSIX互斥锁和条件变量。 - CubicleSoft
3
不行,因为在调用pthread_cond_signal/broadcast时,你需要在mutex周围加锁,但是在调用pthread_cond_wait之前无法这样做。 - a3f
2
@Quuxplusone - 不行,有两个原因。首先,条件变量不是电平触发的,而是边缘触发的。因此,如果线程1在线程3调用notify后无条件开始等待,它将不会从等待中唤醒以处理线程3排队的项目。即使前面的原因不适用,你也不想引入等待和唤醒的延迟,只是为了出队下一个工作项(如果已经存在)。“项目已经存在”的情况应该是你的快速路径。只有在没有要做的事情时才进入condvar等待路径(慢路径)。 - acm
2
@Quuxplusone 是的,无法避免循环。这正是为什么C++的std::condition_variable允许您传递一个lambda来描述谓词的原因。该入口点将自动在循环中评估lambda,直到满足谓词为止。这样,您就不会使用它错误了。 - acm
显示剩余8条评论

84
以下解释来自 David R. Butenhof 的《POSIX 线程编程》(第80页):
在某些多处理器系统上,让条件变量的唤醒完全可预测可能会大大减慢所有条件变量操作,因此出现虚假唤醒可能听起来很奇怪。
在下面的 comp.programming.threads 讨论中,他进一步阐述了设计背后的思路:
Patrick Doyle写道: > 在文章中,Tom Payne写道: > >Kaz Kylheku写道: > >:这是因为实现有时无法避免插入这些虚假的唤醒;防止它们可能是代价高昂的。
> >但是为什么?例如,我们是否在谈论等待超时恰好在信号到达时的情况?
> 你知道,我想知道pthread的设计者是否使用了这样的逻辑: > 条件变量的用户必须在退出时检查条件,因此如果我们允许虚假唤醒,我们不会给他们增加任何负担; > 并且由于允许虚假唤醒可能使实现更快,所以只有允许它们才会有帮助。
> 他们可能没有考虑任何特定的实现。
你的想法实际上并没有错,只是你没有深入探究。
意图是通过要求谓词循环来强制正确/健壮的代码。这是由“核心线程”中的可证明正确的学术派别推动的, 尽管一旦他们理解了它的含义,我认为没有人真正反对这个意图。
我们用几个层次的理由来遵循这个意图。首先,“宗教般地”使用循环可以保护应用程序免受其自身不完美的编码实践的影响。 第二个是,抽象地想象出机器和实现代码可以利用这个要求来通过优化同步机制来提高平均条件等待操作的性能。
/------------------[ David.Buten...@compaq.com ]------------------\ | Compaq Computer Corporation POSIX线程架构师 | | 我的书:http://www.awl.com/cseng/titles/0-201-63392-2/ | \-----[ http://home.earthlink.net/~anneart/family/dave.html ]-----/

57
基本上这里什么也没说。除了最初的想法“可能会使事情更快”外,没有给出任何解释,并且没有人知道它是否真的可以以及如何实现。 - Bogdan Ionitza
有人找到了一个解释为什么这可能会加快速度的来源吗?当然,从"数学上"来说,拥有更多的自由意味着它不可能更慢,但有人在利用这一点吗? - undefined

8

pthread_cond_signal的“多个条件信号唤醒”章节中,有一个示例实现了pthread_cond_wait和pthread_cond_signal,其中包含虚假唤醒。


2
我认为这个答案在某种程度上是错误的。该页面上的示例实现具有与“通知所有”等效的“通知一个”的实现;但似乎并不会生成实际的虚假唤醒。唯一的唤醒线程的方式是通过其他线程调用“通知所有”,或者通过其他线程调用标记为“通知一个”的东西-它实际上是“通知所有”。 - Quuxplusone
@Quuxplusone 1. 这个例子来自于《POSIX程序员手册》,它可能对所说的内容是正确的。able_to_run(sleeper); 这个语句会唤醒cond->waiter队列的头元素value == cond->value 不满足条件,所以被中断的线程(尚未放入队列,即没有运行 cond->waiter = me;也会在 pthread_mutex_unlock(cond->mutex); /* 12 */ 之后被唤醒。然后就会发生虚假唤醒。 - undefined

7

虽然我认为在设计时并没有考虑这个问题,但以下是一个实际的技术原因:与线程取消相结合,有些情况下"假唤醒"可能是绝对必要的,至少除非你愿意对可行的实现策略施加非常非常强的限制。

关键问题是,如果线程在pthread_cond_wait阻塞时执行取消操作,副作用必须就像它没有消耗过条件变量上的任何信号一样。然而,很难(并且高度受限制)确保您在开始执行取消操作时尚未消耗信号,在这个阶段重新发布信号到条件变量可能是不可能的,因为调用pthread_cond_signal的调用方已经有理由销毁condvar并释放其所在的内存。

允许虚假唤醒可以让您轻松解决问题。如果您在等待条件变量时已经消耗了信号(或者您想偷懒,无论如何),那么当取消操作到达时,您可以声明发生了假唤醒,并成功返回。这不会干扰取消操作的操作,因为正确的调用方将在下一次循环中调用pthread_cond_wait时处理挂起的取消操作。


1
我认为虚假唤醒的主要原因是EINTR

EINTR 中断的系统调用(POSIX.1-2001);请参见 signal(7)。

来源:https://man7.org/linux/man-pages/man3/errno.3.html

基本上,例如通过pthread_cond_wait()调用的系统调用,例如futex(2),可能会返回EINTR。如果一个在内核中被阻塞的系统调用被 POSIX 信号(请参阅signal(7))中断,通常会发生这种情况。请参考unix.stackexchange.com上的"What is the rationale behind EINTR?",了解为什么(一些)操作系统在 POSIX 信号传递和处理后中断系统调用时返回EINTR
我认为,一旦用于实现 pthread_cond_wait() 的低级操作系统原语返回 EINTR,就存在潜在的竞争条件。 pthread_cond_wait() 的实现可能不能简单地重新发出系统调用,因为此时条件可能已经成立。如果在 EINTR 后未重新评估条件,则很容易导致应用程序陷入死锁,无法再取得进展。

0
acm的例子类似于《操作系统导论》书中的“Figure 30.9”,问题在于调度器会使唤醒线程在另一个已调度的消费者之后运行。
但是,《操作系统导论》第14页的“还处理了虚假唤醒的情况”说它与虚假唤醒不完全相同。从man 3 pthread_cond_broadcast的定义来看,虚假唤醒的定义“可能有多个线程返回”,在acm的例子中没有反映出来(即“线程2需要再次等待条件”只意味着唤醒了一个线程),所以《操作系统导论》认为这不是一个虚假唤醒。
关于man 3 pthread_cond_broadcast的示例的详细信息,可以作为对姚景国答案的补充。还请参阅我在姚景国答案中的评论
依我之见,这个示例实现可能只是为了表示,看起来有些奇怪,因为它在被中断后会继续运行,而不是等待cond->value改变
正如NPE的回答所说,实现是有意为之简化的选择。在我看来,这是因为考虑到了Mesa风格,这也在OSTEP书中提到了。"Mesa风格"意味着在唤醒后使用while重新检查条件,因此在pthread_cond_wait和pthread_cond_signal中避免虚假唤醒可能是多余的。NPE的回答中提到的"条件变量的使用者无论如何都必须在退出时检查条件"也暗示了在pthread_cond_wait之外使用while()的惯例,这被认为是使用pthread_cond_wait的一种约定。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接