std::condition_variable notify_one后wait获锁前行为及测试异常咨询
你遇到的这个随机性问题,核心是操作系统线程调度的不可预测性,再加上std::condition_variable的唤醒机制特性,咱们一步步拆解清楚:
为什么会出现两种不同的运行表现?
场景1:程序阻塞,只输出notify的前两行
这种情况里,notifyTask线程先抢到了mu互斥锁,执行完i=0和打印操作后调用cv.notify_one()——但这时候mainTask线程还没进入cv.wait(lk)的等待状态(要么还在排队抢mu锁,要么刚进入函数还没走到wait那一步)。这个notify信号相当于"白发"了,之后notifyTask抱着锁休眠5秒,结束后才释放锁。等mainTask终于拿到锁进入wait时,已经没有新的通知信号了,就一直阻塞下去。
场景2:休眠5秒后正常输出main task
这种场景下,mainTask线程先拿到mu锁,进入cv.wait(lk)——这里wait会自动释放锁,然后进入等待状态。之后notifyTask抢到锁,完成设置i、打印、调用cv.notify_one()的操作。这时候mainTask被唤醒,但它需要重新获取mu锁才能继续执行,而notifyTask还抱着锁在休眠,所以mainTask只能等5秒,等notifyTask释放锁后才能拿到锁,继续执行i++和打印,程序正常结束。
线程的调度顺序完全由操作系统内核决定,没有固定规律,这就是你看到两种不同表现的根本原因。
怎么修复这个问题?
要避免信号丢失和调度顺序带来的不确定性,给std::condition_variable搭配条件谓词是行业最佳实践,同时要确保通知线程先修改共享状态,再发送信号。
修改后的代码如下:
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> class CondWait{ public: std::condition_variable cv; std::mutex mu; int i=0; // 新增条件标志,用来判断是否可以继续执行 bool ready = false; public: void mainTask(){ std::unique_lock<std::mutex> lk(mu); // wait搭配谓词,既避免虚假唤醒,也解决信号丢失问题 cv.wait(lk, [this](){ return ready; }); i++; std::cout<<"main task, "<<i<<std::endl; } void notifyTask(){ std::unique_lock<std::mutex> lk(mu); i = 0; ready = true; // 先设置条件标志 std::cout<<"notify task, "<<i<<std::endl; cv.notify_one(); // 再发送通知 std::cout<<"notify task, sleep 5 sec"<<std::endl; std::this_thread::sleep_for(std::chrono::seconds(5)); } }; int main() { CondWait condwait; std::thread t1(&CondWait::mainTask,&condwait); std::thread t2(&CondWait::notifyTask,&condwait); t1.join(); t2.join(); return 0; }
修复逻辑说明:
- 新增的
ready标志作为共享的条件谓词,mainTask只有在ready为true时才会结束wait - 不管线程调度顺序如何:
- 如果
mainTask先进入wait,它会释放锁等待ready变为true,当notifyTask设置ready=true并发通知后,mainTask会被唤醒,重新获取锁后检查谓词为真,继续执行 - 如果
notifyTask先执行,它会先设置ready=true再发通知,之后mainTask拿到锁进入wait时,检查谓词已经为真,会直接跳过wait继续执行,不会阻塞
- 如果
另外还有个小优化:如果notifyTask不需要在休眠时持有锁,可以在notify之后提前解锁,这样mainTask被唤醒后能立刻拿到锁执行,不用等5秒休眠结束:
void notifyTask(){ std::unique_lock<std::mutex> lk(mu); i = 0; ready = true; std::cout<<"notify task, "<<i<<std::endl; cv.notify_one(); lk.unlock(); // 提前解锁,让mainTask可以立刻获取锁执行 std::cout<<"notify task, sleep 5 sec"<<std::endl; std::this_thread::sleep_for(std::chrono::seconds(5)); }
内容的提问来源于stack exchange,提问作者tingda zhuang

