为什么pthread_cond_wait有虚假的唤醒?

Jon*_*vis 136 c pthreads

引用手册页:

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

因此,pthread_cond_wait即使您没有发信号也可以返回.乍一看,这看起来非常残酷.它就像一个随机返回错误值或在实际到达正确的返回语句之前随机返回的函数.这似乎是一个主要的错误.但是他们选择在手册页中记录这一点而不是修复它的事实似乎表明有一个合理的理由为什么pthread_cond_wait最终虚假地唤醒.据推测,它有一些内在的关于它是如何工作的,这使它无法帮助.问题是什么.

为什么没有pthread_cond_wait虚假回报?为什么它不能保证它只是在它被正确发出信号后醒来?谁能解释其虚假行为的原因?

acm*_*acm 98

至少有两件事"虚假唤醒"可能意味着:

  • 被阻止的线程pthread_cond_wait可以从呼叫返回,即使在条件发生时没有呼叫信号或广播.
  • pthread_call_signal由于调用信号或广播而阻塞返回的线程,但是在重新获取互斥锁之后,发现基础谓词不再为真.

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

  • 线程1刚刚将一个元素出列并释放了互斥锁,队列现在为空.该线程正在对它在某些CPU上获取的元素执行任何操作.
  • 线程2尝试使元素出列,但pthread_cond_broadcast在等待信号/广播的呼叫中的互斥,呼叫和块下检查时,发现队列为空.
  • 线程3获取互斥锁,将新元素插入队列,通知条件变量,并释放锁.
  • 响应来自线程3的通知,等待该条件的线程2被安排运行.
  • 但是,在线程2设法进入CPU并获取队列锁之前,线程1完成其当前任务,并返回队列以进行更多工作.它获取队列锁,检查谓词,并发现队列中有工作.它继续将线程3插入的项目出列,释放锁定,并对线程3排队的项目执行任何操作.
  • 线程2现在进入CPU并获得锁定,但是当它检查谓词时,它发现队列为空.线程1'偷走'该项目,因此唤醒似乎是虚假的.线程2需要再次等待该条件.

因此,您始终需要在循环下检查谓词,如果基础条件变量可以具有其他类型的虚假唤醒,则没有区别.

  • 是.实际上,这是在使用事件而不是具有计数的同步机制时发生的情况.可悲的是,似乎POSIX信号量(无论如何在Linux上)也受到了spurius唤醒的影响.我发现有点奇怪的是,同步原语的基本功能失败只是被接受为"正常"并且必须在用户级别处理:(据推测,如果记录系统调用,开发人员将会紧张起来有一个'Spurious segfault'部分,或者,'Spurious连接到错误的URL'或'Spurious打开错误的文件'. (23认同)
  • "虚假唤醒"的更常见情况很可能是调用pthread_cond_broadcast()的副作用.假设你有一个5个线程的池,两个唤醒广播并完成工作.其他三个人醒来后发现工作已经完成.多处理器系统还可能导致条件信号意外唤醒多个线程.代码只是再次检查谓词,看到无效状态,然后再回到睡眠状态.在任何一种情况下,检查谓词都可以解决问题.通常,IMO用户不应使用原始POSIX互斥和条件. (2认同)
  • @Yola不,它不能,因为你应该在`pthread_cond_signal/broadcast`周围锁定一个互斥锁,你将无法这样做,直到通过调用`pthread_cond_wait`来解锁互斥锁. (2认同)
  • @Quuxplusone - 不,这不起作用,有两个原因。首先,条件变量不是电平触发的——它们是边沿触发的。因此,如果线程1在线程3调用notify后开始无条件等待,它不会从等待中唤醒来处理线程3排队的项目。即使先前的原因在某种程度上不适用,您也不想引入等待和唤醒的延迟,只是为了使下一个工作项(如果它已经存在)出列。“项目已存在”情况应该是您的快速路径。你只想在无事可做的情况下进入 condvar 等待路径(慢速路径)。 (2认同)
  • @acm:我的“修复”假设总共只有与获取工作项的线程一样多的通知。例如,如果我们仅使用它来表示“嘿,线程 B,线程 A 已完成并准备好开始工作!” 同意这对于一般的工作队列来说不是一个高性能(甚至可能是正确/有用)的想法。但从昨天开始,我找到了一种在 Linux 上进行虚假唤醒的方法:`kill -19 <pid>; Kill -18 <pid>` 将虚假唤醒目标进程中的所有 futex。所以这是没有意义的;虚假唤醒确实是真实的,因此即使在我的情况下,循环也是 100% 需要的。 (2认同)
  • @Quuxplusone 是的,没有绕过循环。这实际上就是为什么 C++ `std::condition_variable` 允许您传入 lambda 来描述谓词。该入口点将自动在循环中计算 lambda,直到满足谓词为止。这样你就不会用错了。 (2认同)

NPE*_*NPE 75

David R. Butenhof在"使用POSIX线程编程"(第80页)中给出了以下解释:

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

在下面的comp.programming.threads讨论中,他扩展了设计背后的思路:

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 ]-----/ 

  • 基本上这没什么。除了最初的想法“它可以使事情变快”之外,这里没有给出任何解释,但是没人知道它是如何或是否这样做。 (12认同)

Jin*_*Yao 7

pthread_cond_signal中的"条件信号的多次唤醒"部分具有pthread_cond_wait和pthread_cond_signal的示例实现,其涉及虚假唤醒.

  • 我认为这个答案是错误的,就此而言.该页面上的示例实现具有"通知一个"的实现,这相当于"通知所有"; 但它似乎并没有产生*虚假*唤醒.线程唤醒的唯一方法是通过调用"全部通知"的其他线程,或者通过调用-thing-labeled-"通知一个"的其他线程来实现 - "通知所有". (2认同)

R..*_*R.. 5

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

关键问题是,如果一个线程在被阻塞时执行了取消操作pthread_cond_wait,那么副作用就好像它没有消耗条件变量上的任何信号一样。然而,当您开始对取消采取行动时,很难(并且受到高度限制)确保您尚未消耗信号,并且在此阶段可能无法将信号“重新发布”到条件变量,因为您可能在这种情况下,调用者pthread_cond_signal已经有理由销毁 condvar 并释放它所在的内存。

虚假唤醒的津贴让您轻松出局。如果您可能已经消耗了一个信号(或者如果您想偷懒,无论如何),而不是在它到达时在条件变量上阻塞时继续对取消采取行动,您可以声明一个虚假的唤醒已经发生,并成功返回。这根本不会干扰取消的操作,因为正确的调用者将在下一次循环并pthread_cond_wait再次调用时对挂起的取消进行操作。