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

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

TL;DR

reference 说:

打算修改变量的线程必须

  1. 获取 std::mutex(通常通过 std::lock_guard)
  2. 在持有锁时执行修改
  3. 在 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

非常感谢您的任何回答!

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

【问题讨论】:

    标签: c++ c++11 concurrency semantics condition-variable


    【解决方案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 发出信号无关。
    • 我认为这只是为了理论理解而对问题过于简单化的一个例子,而不是我的实际代码中的糟糕设计:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多