C++11 条件变量语义

Posted

技术标签:

【中文标题】C++11 条件变量语义【英文标题】:C++11 condition variable semantics 【发布时间】:2020-04-18 13:50:34 【问题描述】:

我试图理解std::condition_variable 的语义。我以为我对 C++11 并发模型(原子、内存排序、对应的guarantees and formal relations)有相当的了解,但是关于如何正确使用条件变量的描述似乎与我的理解相矛盾。

TL;DR

reference 说:

打算修改变量的线程必须

    获取 std::mutex(通常通过 std::lock_guard) 在持有锁时执行修改 在 std::condition_variable 上执行 notify_one 或 notify_all(通知不需要持有锁)

即使共享变量是原子的,也必须在互斥体下进行修改,才能正确地将修改发布到等待线程。

我明白为什么修改可能必须在释放互斥锁之前完成,但上面似乎很清楚,它必须同时持有互斥锁,即它不能在获取之前。我读对了吗?

更详细

如果我对上述内容的理解是正确的,那么为什么会这样呢?考虑我们在关键部分之前进行修改(通过正确使用原子和锁来确保没有竞争条件)。例如

std::atomic<bool> dummy;
std::mutex mtx;
std::condition_variable cv;

void thread1() 
    //...
    // Modify some program data, possibly in many places, over a long period of time
    dummy.store(true, std::memory_order_relaxed); // for simplicity
    //...
    mtx.lock(); mtx.unlock();
    cv.notify_one();
    //...


void thread2() 
    // ...
     std::unique_lock<std::mutex> ul(mtx);
        cv.wait(ul, []() -> bool 
            // A complex condition, possibly involving data from many places
            return dummy.load(std::memory_order_relaxed); // for simplicity
        );
    
    // ...


我的理解是cv.wait() 在继续之前锁定mtx(检查条件并执行程序的其余部分)。此外,std::mutex::lock() 算作 acquire 操作,std::mutex::unlock() 算作 release 操作。这是否意味着 thread1 中的 unlock() 同步 thread2 中的 lock(),因此在 unlock() 之前在 thread1 中执行的所有原子甚至非原子存储对 thread2 都是可见的它醒来了吗?

Formally:  store --sequenced-before--> unlock() --synchronizes-with--> lock() --sequenced-before--> load
...and so: store --happens-before--> load

非常感谢您的任何回答!

[注意:经过大量谷歌搜索后,我还没有找到答案,这很奇怪;如果是重复的,我很抱歉...]

【问题讨论】:

【参考方案1】:

考虑在thread1中锁定互斥锁之前的时间和condition_variable首先解锁thread2中的互斥锁之前的时间。

线程1可以

修改大量程序数据 dummy.store(true, std::memory_order_relaxed)

线程2可以

锁定互斥锁 dummy.load(std::memory_order_relaxed)(等待前检查谓词)

彼此之间没有顺序。如果 thread2 在此检查中看到 dummy 的真值并继续,则不能保证任何数据修改对 thread2 可见。 thread2 将继续运行,正确地看到了 dummy 的值,但没有正确地看到修改。

您说“通过正确使用原子和锁来确保没有竞争条件”,这是非常开放的。宽松的原子将是正确的,并且修改不一定在 thread2 中可见。但是,围绕这些其他数据修改进行假设的额外同步可以保证可见性。

换句话说,在存储和加载之间应该有一些发布-获取顺序。

这类似于:waiting on worker thread using std::atomic flag and std::condition_variable

【讨论】:

好的,非常感谢。该链接回答了我的问题。 (仅供阅读本文的任何人参考:)我认为您已经错过了互斥锁已锁定和解锁的事实,该互斥锁已经用作释放-获取对(如链接下的第一个答案中所述),所以我' m 现在确信上面的代码是正确的。 @EMarci15 我不认为你在上面的评论中做出了正确的结论。 @EMarci15 你的问题和链接的问题之间的区别是memory_order_relaxed vs memory_order_release 写入原子标志。 @EMarci15 好的,这很公平。但是,考虑到以这种方式使用条件变量至少可以说是非常“不习惯”的。建议程序员在循环中检查条件等等是有原因的。使条件变量按预期工作是一件棘手的事情。仅从代码审查的角度来看,我建议以这样一种方式设计同步,即您在 T2 中实现的预期效果基于某种状态不变量,该不变量与 T1 是否设法向 T2 发出信号无关。 我认为这只是为了理论理解而对问题过于简单化的一个例子,而不是我的实际代码中的糟糕设计:)

以上是关于C++11 条件变量语义的主要内容,如果未能解决你的问题,请参考以下文章

[C++11 多线程同步] --- 条件变量的那些坑条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)

[C++11 多线程同步] --- 条件变量的那些坑条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)

条件变量中 wait_for 函数的用途 - C++11

[多线程]C++11多线程-条件变量(std::condition_variable)

C++11多线程 条件变量condition_variable

[C++11 多线程同步] --- 条件变量