在没有锁的情况下设置条件变量监控标志是否有效?

问题描述 投票:0回答:1

以下代码使用条件变量和监视器标志来同步主线程和线程2之间的操作:

int main() {
    std::mutex m;
    std::condition_variable cv;
    std::atomic<bool> ready = false;
    std::thread thread2 = std::thread([&](){
        std::unique_lock<std::mutex> l(m);
        cv.wait(l, [&ready]{return ready.load();});
        std::cout << "Hello from thread2\n"; // 3 should print after 1
    });
    std::cout << "Hello from main thread\n"; // 1 we want this to be 1st
    ready = true; // 2, store to an atomic bool, without a lock, is it OK?
    cv.notify_one();
    thread2.join();
    std::cout << "Goodbye from main thread\n";
} 

在上面的代码中,我们使用

atomic<bool>
作为监视器标志
ready
,因此对此标志的读取和写入不会产生数据争用(对于大多数(如果不是全部)平台而言,这不是问题,但仍然是 UB“by书”)并避免对标有 1 和 2 的行进行重新排序(原子变量的默认 store
memory_order_seq_cst
,这保证了该线程中存储之前发生的所有事情都将是执行此变量加载的线程)。

但是,代码不会锁定

ready
标志的修改(这是原子性的)和对
notify_one
的调用。

this SO post 可以清楚地看出,在没有锁的情况下将调用留给

notify_one
是可以的,甚至可能会更有效,因为我们不希望 thread2 在调用
notify_one
之后处于唤醒状态,然后请注意,它应该等待锁定并由操作系统调度程序发送休眠状态,直到锁定被释放。

但是,尚不清楚

ready
标志的修改是否应在锁定范围内完成(使用与读取相同的
mutex
),还是使用
atomic<bool>
就足够了?

c++ multithreading synchronization wait condition-variable
1个回答
0
投票

ready
标志的更新必须使用用于读取的相同互斥锁来锁定

(然后布尔值可能会变成简单的布尔值,而不是

atomic<bool>
)。

根据cppreference

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

这篇博客文章很好地解释了为什么需要锁,以及为什么使用原子是不够的。类似的解释可以在this SO post(在相关的类似场景中)和this extra SO post中找到,其中列出了即使对于原子变量也使用锁的原因。一个非常相似的问题已经在herehere进行了讨论和解释。


问题

如果没有锁,我们可能会陷入以下竞争状态:

  1. Thread2 检查
    ready
    标志,它是 false,它计划开始等待条件变量(通过调用基本的
    cv.wait(lock)
    操作),但它仍然是在此调用之前。
  2. 主线程将
    ready
    标志设置为
    true
    ,并且在线程 2 尚未等待条件变量时足够快地调用
    cv.notify_one()
  3. Thread2 现在调用
    cv.wait(lock)
    并永远挂起,因为通知已发送并“丢失”。

让我们证明一下竞争条件

为了证明未锁定时的竞争条件是真实的,我们可以在 thread2 中添加一个睡眠来模拟有效的计时场景:

std::thread thread2 = std::thread([&](){
    std::unique_lock<std::mutex> l(m);
    cv.wait(l, [&ready]{
        auto r = ready.load();
        std::this_thread::sleep_for(20ms); // timing that causes missing the event
        return r;
    });

添加这个睡眠实际上使thread2挂起,QED:锁定

ready
标志的修改是绝对需要的,这不仅仅是理论上的


解决方案:锁定!

下面的版本解决了这个问题,通过锁定

ready
标志的修改,我们现在不需要标志为
atomic

int main() {
    std::mutex m;
    std::condition_variable cv;
    bool ready = false;
    std::thread thread2 = std::thread([&](){
        std::unique_lock<std::mutex> l(m);
        cv.wait(l, [&ready]{ return ready; });
        std::cout << "Hello from thread2\n";
    });
    std::cout << "Hello from main thread\n";
    // synchronization block
    {
        std::unique_lock<std::mutex> l(m);
        ready = true;
    }
    cv.notify_one();
    thread2.join();
    std::cout << "Goodbye from main thread\n";
}

它如何解决上面提到的竞赛?

当锁由线程 2 拥有时,主线程无法将

ready
设置为
true
。由于锁由线程 2 拥有,直到它在内部被释放为止,所以对
cv.wait(lock)
的调用(仅当等待开始时锁才会被释放),主线程将无法在线程 2 开始等待之前修改
ready
标志条件变量。因此,当 thread2 已经处于等待状态时,保证会发生 main 中对
cv.notify_one()
的调用。


超时等待

需要注意的是,我们可能更喜欢超时等待(这是一个一般性的好建议,更喜欢超时等待,以避免死锁并更好地跟踪线程状态)。如果我们等待超时,我们可能会决定放弃锁定并返回

atomic<bool>
,像这样

int main() { std::mutex m; std::condition_variable cv; std::atomic<bool> ready = false; std::thread thread2 = std::thread([&](){ std::unique_lock<std::mutex> l(m); // adding a timeout while(!cv.wait_for(l, 100ms, [&ready]{ return ready.load(); })); std::cout << "Hello from thread2\n"; }); std::cout << "Hello from main thread\n"; ready = true; // no lock, cv.wait_for prevents us from hanging cv.notify_one(); thread2.join(); std::cout << "Goodbye from main thread\n"; }
    
© www.soinside.com 2019 - 2024. All rights reserved.