为std::condition_variable设置就绪标志的正确方式
兄弟,听我一句劝,你可千万别把signaler里那行std::lock_guard的注释留着!哪怕你用的是std::atomic<bool>类型的ready,也必须在设置它的时候锁住对应的mtx互斥量,我给你掰扯清楚这里面的门道:
1. 彻底躲开「丢失唤醒」的大坑
你想啊,要是不锁这个互斥量,搞不好就会碰到这种倒霉情况:worker线程刚拿到mtx,检查ready是false,正准备释放锁进入等待队列的那一瞬间,signaler线程直接把ready设为true还调用了notify_one。这时候worker还没真正挂到condition_variable的等待队列上,这个唤醒信号就等于白发了——worker之后会一直卡在wait里等一个永远不会来的信号,这就是圈内人常说的lost wakeup(丢失唤醒),踩过这个坑的人都懂有多闹心。
但你要是给signaler里设置ready的代码加上lock_guard,就能从根源上避免这种情况:要么signaler先拿到锁,把ready设为true再发唤醒信号,这时候worker之后调用wait时,一检查ready就是true,直接跳过等待;要么worker先拿到锁,进入wait后释放锁,signaler才能拿到锁设置ready并发信号,这时候worker肯定已经在等待队列里了,绝对不会错过唤醒。
2. 符合C++标准的同步规则
虽然std::atomic本身能保证内存可见性,但C++标准里对condition_variable的使用有明确要求:修改等待条件(也就是这里的ready)的操作,必须和wait调用处于同一个同步序列里——说白了就是得持有同一个互斥量。这样才能确保被唤醒的worker线程,能100%看到ready的最新值,不会因为CPU缓存或者指令重排搞出莫名其妙的内存可见性问题。
修正后的完整代码
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <atomic> #include <chrono> std::mutex mtx; std::condition_variable cv; std::atomic<bool> ready = false; void worker() { std::unique_lock<std::mutex> lock(mtx); std::cout << "Waiting for signal...\n"; cv.wait(lock, [] { return ready.load(); }); std::cout << "Continue\n"; } void signaler() { std::this_thread::sleep_for(std::chrono::seconds(2)); { std::lock_guard<std::mutex> lock(mtx); // 这行必须加上! ready = true; std::cout << "Data ready\n"; } cv.notify_one(); } int main() { std::thread t1(worker); std::thread t2(signaler); t1.join(); t2.join(); return 0; }
说白了,哪怕用了atomic变量,condition_variable配套的互斥量也得老老实实锁上——这不是多此一举,是从根源上避免各种诡异线程竞态问题的关键!
内容来源于stack exchange

