在 condition_variable::notify_all() 之后或之前解锁互斥锁?

Posted

技术标签:

【中文标题】在 condition_variable::notify_all() 之后或之前解锁互斥锁?【英文标题】:unlock the mutex after condition_variable::notify_all() or before? 【发布时间】:2018-09-25 17:01:34 【问题描述】:

查看several videos 和documentation example,我们调用notify_all() 之前解锁互斥锁。之后调用它会更好吗?

常用方式:

Notifier 线程内部:

//prepare data for several worker-threads;

//and now, awaken the threads:
std::unique_lock<std::mutex> lock2(sharedMutex);
_threadsCanAwaken = true;

lock2.unlock(); 
_conditionVar.notify_all(); //awaken all the worker threads;

//wait until all threads completed;

//cleanup:
_threadsCanAwaken = false;

//prepare new batches once again, etc, etc

在其中一个工作线程内:

while(true)
    // wait for the next batch:

    std::unique_lock<std::mutex> lock1(sharedMutex);
    _conditionVar.wait(lock1,  []()return _threadsCanAwaken);
    lock1.unlock(); //let sibling worker-threads work on their part as well

    //perform the final task

    //signal the notifier that one more thread has completed;

    //loop back and wait until the next task

注意lock2 在我们通知条件变量之前是如何解锁的 - 我们是否应该在notify_all() 之后 解锁它?

编辑

来自我下面的评论:我担心的是,如果消费者超级快怎么办。消费者虚假地醒来,看到互斥锁被解锁,完成任务并循环回到 while 的开始。现在,slow-poke Producer 最终调用了 notify_all(),导致 consumer 循环了额外的时间。

【问题讨论】:

lock2 的析构函数调用将在_conditionVar.notify_all(); 之后自动解锁互斥锁,因此您根本不需要显式调用它,这是常见的成语IIRC。顺便说一句,不要在任何代码中使用前缀下划线,这是为编译器和标准库实现保留的。 谢谢!我担心的是,如果消费者超级快怎么办。消费者突然醒来,看到互斥锁已解锁,完成任务并循环回到while 的开头。现在慢戳生产者终于调用notify_all(),导致消费者循环额外的时间。 【参考方案1】:

除非您的实现不寻常,否则在发出条件变量信号之前解锁互斥锁没有任何好处。在发信号之前解锁有两个缺点:

    如果您在发出信号之前解锁,则信号可能会唤醒选择在您解锁后阻塞条件变量的线程。如果您使用相同的条件变量来表示多个逻辑条件,这可能会导致死锁。这种错误很难创建,很难诊断,也很难理解。通过在解锁前始终发出信号可以轻松避免这种情况。这确保了共享状态和信号的更改是原子操作,并且不可能出现竞争条件和死锁。

    信号前解锁会降低性能,而信号后解锁可以避免这种损失。如果您在解锁之前发出信号,一个好的实现将知道您的信号不可能使任何线程准备好运行,因为互斥锁由调用线程持有,并且受条件变量影响的任何线程在没有互斥锁的情况下必然无法向前推进.这允许进行重大优化(通常称为“等待变形”),如果您先解锁,这是不可能的。

所以,除非你有什么不寻常的理由,否则请在持有锁时发出信号。

【讨论】:

【参考方案2】:

我们应该在 notify_all() 之后解锁它吗?

无论哪种方式都是正确的,但在不同的情况下您可能会有不同的行为。很难预测它将如何影响程序的性能——我已经看到了不同应用程序的正面和负面影响。因此,您最好对您的程序进行概要分析,并根据概要分析决定您的特定情况。

【讨论】:

我理解正确吗, notify_all() 从当前持有的锁中释放互斥锁?对我来说不是很清楚,因为notify_all() 不接受我们当前的锁作为参数(不像wait)。我知道我们的锁会在本地范围结束时被销毁,但不会在notify_all 之后立即销毁? 不,notify_allnotify_one 都不会解锁互斥锁,这是您手动或通过 RAII 机制的工作。如果您在通知之前或之后解锁(我的意思是时间),您将有不同的行为。【参考方案3】:

这里提到:cppreference.com

通知线程不需要在同一个互斥体上持有锁 作为等待线程持有的线程;事实上这样做是一个 悲观化,因为被通知的线程会立即阻塞 再次等待通知线程释放锁。

也就是说,documentation for wait

在阻塞线程的那一刻,函数自动调用 lck.unlock(),允许其他锁定的线程继续。

一旦通知(明确地,由其他线程),函数 解除阻塞并调用 lck.lock(),使 lck 处于与 when 相同的状态 该函数被调用。然后函数返回(注意这个 最后一个互斥锁可能会在返回之前再次阻塞线程)。

所以当通知等待将重新尝试获得锁,并且在该过程中它将再次被阻塞,直到原始通知线程释放锁。 所以我建议在调用通知之前释放锁。正如 cppreference.com 上的示例中所做的那样,最重要的是

不要悲观。

【讨论】:

【参考方案4】:

我担心的是,如果消费者超级快怎么办。消费者 虚假唤醒,看到互斥锁被解锁,完成任务 并循环回到 while 的开头。现在是慢戳制片人 最后调用 notify_all(),导致消费者循环一个额外的 时间。

来自cpp reference:

当通知条件变量时,超时,或 发生虚假唤醒,线程被唤醒,互斥锁被唤醒 原子重新获得。然后线程应该检查条件和 如果唤醒是虚假的,则继续等待。

// Manual unlocking is done before notifying, to avoid waking up
// the waiting thread only to block again (see notify_one for details)
lk.unlock();
cv.notify_one();

【讨论】:

以上是关于在 condition_variable::notify_all() 之后或之前解锁互斥锁?的主要内容,如果未能解决你的问题,请参考以下文章

NOIP 2015 & SDOI 2016 Round1 & CTSC 2016 & SDOI2016 Round2游记

秋的潇洒在啥?在啥在啥?

上传的数据在云端的怎么查看,保存在啥位置?

在 React 应用程序中在哪里转换数据 - 在 Express 中还是在前端使用 React?

存储在 plist 中的数据在模拟器中有效,但在设备中无效

如何在保存在 Mongoose (ExpressJS) 之前在模型中格式化数据