为什么 pthread _ cond _ wait 有虚假的唤醒?

引用手册页:

当使用条件变量时,总有一个布尔谓词涉及与每个条件相关联的共享变量,如果线程继续运行,则为 true。可能会发生 pthread _ cond _ timedwait ()或 pthread _ cond _ wait ()函数的虚假唤醒。由于 pthread _ cond _ timedwait ()或 pthread _ cond _ wait ()的返回并不意味着任何关于该谓词值的内容,因此应该在这种返回时重新计算谓词。

因此,即使您没有发出信号,pthread_cond_wait也可以返回。至少乍一看,这似乎相当糟糕。它就像一个函数,随机返回错误的值,或者在实际到达正确的 return 语句之前随机返回。看起来是个大漏洞。但是,他们选择在手册页中记录而不是修复这个问题的事实似乎表明,pthread_cond_wait最终以虚假的方式醒来是有合理的原因的。可以推测,它的运作方式有其内在的原因,所以这是不可避免的。问题是是什么。

为什么 pthread_cond_wait虚假地返回?为什么它不能保证它只有在收到正确的信号时才会醒来呢?有人能解释一下这种虚假行为的原因吗?

38089 次浏览

下面的解释是由大卫 R。布滕霍夫在 “使用 POSIX 线程编程”(第80页)中给出的:

虚假的唤醒可能听起来很奇怪,但是在一些多处理器系统上,使条件唤醒完全可预测可能会大大减缓所有条件变量操作。

在下面的 线程讨论中,他阐述了设计背后的思想:

Patrick Doyle wrote:
> In article , Tom Payne   wrote:
> >Kaz Kylheku  wrote:
> >: It is so because implementations can sometimes not avoid inserting
> >: these spurious wakeups; it might be costly to prevent them.


> >But why?  Why is this so difficult?  For example, are we talking about
> >situations where a wait times out just as a signal arrives?


> You know, I wonder if the designers of pthreads used logic like this:
> users of condition variables have to check the condition on exit anyway,
> so we will not be placing any additional burden on them if we allow
> spurious wakeups; and since it is conceivable that allowing spurious
> wakeups could make an implementation faster, it can only help if we
> allow them.


> They may not have had any particular implementation in mind.


You're actually not far off at all, except you didn't push it far enough.


The intent was to force correct/robust code by requiring predicate loops. This was
driven by the provably correct academic contingent among the "core threadies" in
the working group, though I don't think anyone really disagreed with the intent
once they understood what it meant.


We followed that intent with several levels of justification. The first was that
"religiously" using a loop protects the application against its own imperfect
coding practices. The second was that it wasn't difficult to abstractly imagine
machines and implementation code that could exploit this requirement to improve
the performance of average condition wait operations through optimizing the
synchronization mechanisms.
/------------------[ David.Buten...@compaq.com ]------------------\
| Compaq Computer Corporation              POSIX Thread Architect |
|     My book: http://www.awl.com/cseng/titles/0-201-63392-2/     |
\-----[ http://home.earthlink.net/~anneart/family/dave.html ]-----/


“虚假的觉醒”至少意味着两件事:

  • pthread_cond_wait中被阻塞的线程可以从调用返回,即使在此条件下没有调用 pthread_call_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需要再次等待条件。

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

Pthread _ cond _ information中的“条件信号引起的多次唤醒”部分提供了一个 pthread _ cond _ wait 和 pthread _ cond _ information 的实现示例,这两个实现涉及虚假唤醒。

虽然我不认为在设计的时候就考虑过这个问题,但这里有一个实际的技术原因: 与线程取消相结合,在某些情况下,选择“虚假地”唤醒可能是绝对必要的,至少除非你愿意对可能的实现策略施加非常非常强大的限制。

关键问题是,如果一个线程在 pthread_cond_wait中阻塞时作用于取消,那么副作用必须是它没有消耗条件变量上的任何信号。然而,当您开始执行取消操作时,要确保您还没有消耗信号是困难的(并且是高度限制的) ,而且在这个阶段可能不可能将信号“重新发布”到条件变量,因为您可能处于这样一种情况,即 pthread_cond_signal的调用者已经有理由销毁 condvar 并释放它所驻留的内存。

允许虚假尾流让你更容易脱身。如果您可能已经使用了一个信号(或者无论如何都想偷懒) ,您可以声明一个虚假的唤醒已经发生,然后成功返回,而不是继续执行取消操作。这根本不会干扰取消操作,因为正确的调用方在下次循环并再次调用 pthread_cond_wait时,只会对挂起的取消进行操作。

我认为造成假觉醒的一个主要原因是 EINTR

中断的函数调用(POSIX.1-2001) ; 请参阅信号(7)。

资料来源: https://man7.org/linux/man-pages/man3/errno.3.html,另见

例如,pthread_cond_wait()(例如 futex(2))调用的系统调用基本上可以与 EINTR一起返回。这通常发生在系统调用(在内核中被阻塞)被 POSIX 信号中断时(参见 signal(7))。如果在系统调用发出线程传递和处理 POSIX 信号后,系统调用被中断,请参考 “ EINTR 背后的理由是什么?” unix.stackexchange.com ,了解为什么(一些)操作系统返回 EINTR

我假设一旦用于实现的低级操作系统原语(例如,pthread_cond_wait()返回 EINTR)出现了潜在的竞态条件。pthread_cond_wait()的实现可能不会简单地重新发出系统调用,因为这个条件现在可能已经成立。如果在 EINTR之后没有重新计算条件,那么这很容易导致死锁,应用程序不会进一步进展。