在未锁定互斥量的情况下调用pthread_cond_signal

95
我在某处读到,我们应该在调用pthread_cond_signal之前锁定mutex,并在调用后解锁mutex:

pthread_cond_signal()例程用于向等待condition变量的另一个线程发出信号(或唤醒)。它应该在锁定mutex之后调用,并且必须在pthread_cond_wait()例程完成之前解锁mutex。

我有一个问题:没有锁定mutex,调用pthread_cond_signalpthread_cond_broadcast方法是否可以?

答案是不行。在调用这些方法之前,必须先锁定mutex以确保线程安全。如果未锁定mutex,则可能会导致意外结果。所以,在调用这些例程之前,请始终锁定mutex,并在调用之后解锁mutex。
3个回答

165
如果在修改条件并发出信号的代码路径中不锁定互斥锁,可能会丢失唤醒。考虑以下这对进程: 进程A:
pthread_mutex_lock(&mutex);
while (condition == FALSE)
    pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);

进程B(错误的):

condition = TRUE;
pthread_cond_signal(&cond);

接下来考虑一种可能的指令交错方式,其中condition最初为FALSE

Process A                             Process B

pthread_mutex_lock(&mutex);
while (condition == FALSE)

                                      condition = TRUE;
                                      pthread_cond_signal(&cond);

pthread_cond_wait(&cond, &mutex);

条件现在为TRUE,但进程A卡在等待条件变量上-它错过了唤醒信号。如果我们修改进程B来锁定互斥锁:

进程B(正确):

pthread_mutex_lock(&mutex);
condition = TRUE;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

如果这样,上述情况就不会发生;唤醒永远不会被错过。

(请注意,您实际上可以将 pthread_cond_signal() 移动到 pthread_mutex_unlock() 之后,但这可能导致线程调度不太优化,并且由于更改条件本身,在这个代码路径中必须已经锁定了互斥量)。


5
@Nemo:是的,在“正确”的路径中,pthread_signal_cond() 可以在互斥锁解锁之后移动,虽然最好不要这样做。或许更准确地说,在调用pthread_signal_cond()时,你已经需要锁定互斥锁来修改条件本身了。 - caf
15
就此而言,似乎在信号发出前还是后解锁的问题实际上是不容易回答的。在信号发出后解锁可以确保低优先级线程无法从高优先级线程那里窃取事件,但是如果您没有使用优先级,那么在信号发出前解锁实际上会减少系统调用/上下文切换的次数,并提高整体性能。 - R.. GitHub STOP HELPING ICE
2
@R.. 你搞反了。在信号增加之前解锁会增加系统调用的数量并降低整体性能。如果在解锁之前发出信号,实现就知道该信号不可能唤醒线程(因为任何在条件变量上阻塞的线程都需要互斥锁来推进),从而可以使用快速路径。如果在解锁之后发出信号,则解锁和信号都可以唤醒线程,这意味着它们都是昂贵的操作。 - David Schwartz
2
@Alceste_: 在POSIX中看到这段文字:"当一个线程在等待条件变量时,已经指定了特定的互斥锁来执行pthread_cond_timedwait()pthread_cond_wait()操作,那么该互斥锁和条件变量之间形成了一种动态绑定,只要至少有一个线程被阻塞在条件变量上,该绑定就会生效。在此期间,任何线程尝试使用不同的互斥锁等待该条件变量的行为是未定义的。" - caf
1
@beemavishnu:不,没有死锁,因为pthread_cond_wait()调用是原子性的,它会释放互斥锁并在条件变量上等待。当被唤醒后,在返回之前重新获取互斥锁。这就是为什么需要将已加锁的互斥锁传递给该函数的原因。 - caf
显示剩余22条评论

55
根据本手册:

pthread_cond_broadcast()pthread_cond_signal()函数可以被线程调用,无论它当前是否拥有互斥锁,而调用pthread_cond_wait()pthread_cond_timedwait()的线程在等待期间与条件变量相关联;但是,如果需要可预测的调度行为,则调用pthread_cond_broadcast()pthread_cond_signal()的线程必须锁定该互斥锁。

可预测的调度行为语句的含义由Dave Butenhof(Programming with POSIX Threads作者)在comp.programming.threads上进行了解释,可在此处找到。

8
+1 是为了链接 Dave Butenhof 的邮件而给的。我一直好奇这个问题,现在我知道了……今天学到了重要的东西。谢谢。 - Nils Pipenbrinck
如果需要可预测的调度行为,则将语句按照所需顺序放置在一个线程中,或使用线程优先级。 - Kaz

8
在你的示例代码中,进程B在未锁定互斥锁的情况下修改了condition。如果进程B仅在修改期间锁定了互斥锁,然后在调用pthread_cond_signal之前解锁了该互斥锁,那么就不会有问题 - 我的理解是正确的吗?
我认为直觉上caf的立场是正确的:在没有拥有互斥锁的情况下调用pthread_cond_signal是一个坏主意。但是caf的示例实际上并不支持这种立场的证据;它只是支持了一个更弱的(实际上是自明的)立场的证据,即除非你首先锁定了互斥锁,否则修改受互斥锁保护的共享状态是一个坏主意。
有人可以提供一些示例代码吗?在其中调用pthread_cond_signal,然后调用pthread_mutex_unlock会产生正确的行为,但在调用pthread_mutex_unlock后调用pthread_cond_signal会产生错误的行为?

5
实际上,我认为我的问题是这个问题的一个副本,答案是:“没问题,你可以完全在不拥有互斥锁的情况下调用pthread_cond_signal。没有正确性问题。但在常见的实现中,你会错过pthread深层次的巧妙优化,因此最好仍然在持有锁的情况下调用pthread_cond_signal。” - Quuxplusone
我在我的回答的最后一段做出了这个观察。 - caf
你在这里提供了一个很好的情景:https://dev59.com/8Gw15IYBdhLWcg3wv-Xa#6419626 请注意,它并没有声称行为是不正确的,它只是提出了一种情况,其中行为可能不如预期。 - user1284631
有可能创建示例代码,其中在pthread_mutex_unlock之后调用pthread_cond_signal可能会导致丢失唤醒,因为信号被“错误”的线程捕获,该线程在看到谓词的更改后阻塞。只有当相同的条件变量可用于多个谓词并且您不使用pthread_cond_broadcast时,这才是一个问题,而这种情况很少见且易碎。 - David Schwartz

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