【问题标题】:Notifying condtion_variable without unlocking mutex still works well. Why?在不解锁互斥锁的情况下通知 condtion_variable 仍然可以正常工作。为什么?
【发布时间】:2021-08-08 19:25:09
【问题描述】:
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
 
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
 
void worker_thread()
{
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{return ready;});
 
    std::cout << "Worker thread is processing data\n";
    data += " after processing";
 
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
 
    //lk.unlock(); /// here!!!!!!!!!!!
    cv.notify_one();
}
 
int main()
{
    std::thread worker(worker_thread);
 
    data = "Example data";
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
 
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';
 
    worker.join();
}

我不知道为什么这段代码在 worker_thread 中的 notify_one 之前没有解锁就可以工作。
我想如果我在通知之前不解锁,唤醒主线程将再次阻塞,因为互斥锁仍由 worker_thread 持有。
之后worker_thread将解锁互斥锁(因为unique_lock在销毁时解锁互斥锁)。
那么没有人可以唤醒沉睡的主线程。

但此代码运行良好,无需在通知之前解锁互斥锁。
这是如何工作的???? (我看了cppreference cmets,但看不懂)

【问题讨论】:

  • 条件变量不能停止等待,直到互斥锁被释放,因为退出等待需要重新获取互斥锁。解锁后通知效率更高,但不是必需的。解锁前通知将使通知以正常的互斥锁获取/阻塞时间为准。
  • 当主线程被唤醒时,它不再在 CV 上等待——它现在在互斥体上等待。当互斥锁被解锁时,主线程被唤醒,因为这只是互斥锁的正常场景,不需要唤醒信号。
  • @JohnFilleau 理论上是的,但实际上操作系统已为此进行了优化,并且没有降级。
  • @sergey 这是个好消息!

标签: c++ multithreading c++11


【解决方案1】:

这里有两件事要谈。首先,为什么它有效,其次,为什么你不想先调用 unlock。

之所以有效,是因为cv.wait(lk, []{return processed;}); 在等待通知时实际上会解锁lk

一些序列。 Main 首先获得锁:

 MAIN                       WORKER
 auto lk = lock();          
                            auto lk = lock(); (blocks)
 cv.wait(lk, condition);
 checks condition; fails
 releases lk
                            wakes up with lk
                            fullfill condition
                            cv.notify_one();
 wakes up from notify
 tries to reget lk, blocks
                            lk.unlock();
 wakes up with lk.
 checks condition; passes

工人首先获得锁:

 MAIN                       WORKER
                            auto lk = lock();
 auto lk = lock(); (blocks)         
                            fullfill condition
                            cv.notify_one();
                            lk.unlock();
 wakes up with lk.
 cv.wait(lk, condition);
 checks condition; passes

对于我们先解锁的情况:

 MAIN                       WORKER
 auto lk = lock();          
                            auto lk = lock(); (blocks)
 cv.wait(lk, condition);
 checks condition; fails
 releases lk
                            wakes up with lk
                            fullfill condition
                            lk.unlock();
                            cv.notify_one();
 wakes up from notify
 gets lk
 checks condition; passes

worker先拿到锁,最后有两种可能:

 MAIN                       WORKER
                            auto lk = lock();
 auto lk = lock(); (blocks)         
                            fullfill condition
                            lk.unlock();
 wakes up with lk.
 cv.wait(lk, condition);
 checks condition; passes
                            cv.notify_one(); (nobody cares)

 MAIN                       WORKER
                            auto lk = lock();
 auto lk = lock(); (blocks)         
                            fullfill condition
                            lk.unlock();
                            cv.notify_one(); (nobody cares)
 wakes up with lk.
 cv.wait(lk, condition);
 checks condition; passes

现在,为什么持有锁更好?

因为 C++ 标准库的作者使它变得更好。库知道 cv 服务员与互斥锁相关联,并且知道该线程当前持有它。

所以实际发生的是:

 MAIN                       WORKER
 auto lk = lock();          
                            auto lk = lock(); (blocks)
 cv.wait(lk, condition);
 checks condition; fails
 releases lk
                            wakes up with lk
                            fullfill condition
                            cv.notify_one(); // knows listener holds mutex, waits for
                            lk.unlock(); // and actually wakes up listening threads
 wakes up from notify
 gets lk
 checks condition; passes

现在有比上述更多的可能性。 condition_variable 有“虚假”唤醒,即使没有人通知你,你也会被唤醒。所以在使用它时必须遵守纪律。

一般规则是双方必须共享一个互斥锁,并且必须在在测试条件更改和通知之间的任何时间点保持锁定。最理想的情况是,锁定应该一直保持到发送通知之后。

这除了防止条件状态本身的竞争条件之外。但是如果你使用原子条件状态,它会避免状态上的竞争条件,但它不满足条件变量的锁定要求。

上述规则 - 在该时间间隔内的某个时间(可能在整个时间间隔内)保持锁定 - 是一种简化,但足以保证您不会丢失通知。完整的规则意味着回到 C++ 的内存模型,并对你的代码所做的事情做痛苦的证明,老实说,我不想再这样做了。所以我使用这个经验法则。

为了说明原子条件“状态”和没有锁会出现什么问题;

 MAIN                       WORKER
 auto lk = lock();
 cv.wait(lk, condition);
 checks condition; fails
                            fullfill condition
                            cv.notify_one(); (nobody cares)
 goes to sleep, releases lk

然后什么都不会再发生了。

【讨论】:

  • 好的。我明白为什么在这样通知之前解锁互斥锁:当一个线程通知 cv 时,等待线程唤醒。但是被唤醒的线程将尝试重新获取锁,但它会被阻塞,因为通知线程仍然持有锁(通过通知唤醒线程应该在检查谓词之前重新获取锁,所以被唤醒的线程现在无法检查谓词)。之后通知线程将释放锁,因为 unique_lock 在销毁时释放锁。现在唤醒线程已解除阻塞并检查谓词。你说“虚假唤醒”表示“唤醒线程再次被阻塞,因为通知线程
  • @SungJinKang 我不明白你的解释,抱歉。虚假唤醒——没有通知的唤醒——只是发生,它们是条件变量合同的一部分。当其他人持有锁时,它们可能会发生也可能不会发生;如果其他人持有锁,他们会阻止它。如果没有,他们将获得锁,检查条件,并以其他方式行事,就好像有人通知了一样。
  • 对不起。我说的是“快点等待”的问题。我的理解对吗?? "被唤醒的线程再次被阻塞,因为通知线程还没有释放锁。通知线程释放锁后,被唤醒的线程可以检查谓词"
  • @SungJinKang 是的,理论上,通知可以反过来阻塞互斥锁,或者互斥锁解锁可能会错过信号。在实践中,根据标准库实现者的说法,他们告诉我我做错了,正确的顺序是先发出信号,然后解锁。
猜你喜欢
  • 1970-01-01
  • 2015-09-17
  • 2010-11-18
  • 1970-01-01
  • 2016-04-28
  • 2023-03-04
  • 1970-01-01
  • 2018-05-23
相关资源
最近更新 更多