析构函数中通知condition_variable为何随机丢失通知?
notify_all()会导致随机挂起? 让我们直接拆解你的问题——核心是你对C++标准中condition_variable析构要求的误解,再加上代码里的竞态条件,共同导致了随机挂起。
一、先澄清标准里的关键语义
你引用的标准段落里,有两个容易被误解的核心点:
要求:不得有线程阻塞于this。[注:即所有线程均已被通知;它们后续可能阻塞于等待中指定的锁。这放宽了通常的规则,即要求所有等待调用在销毁前完成。仅解除等待的通知需在销毁前发出。用户应确保一旦析构函数启动,就无线程在this上等待,尤其是当等待线程在循环中调用等待函数,或使用带谓词的wait、wait_for或wait_until重载时。——结束注]
- “阻塞于*this”的定义:指线程正处于
condition_variable的等待队列中——也就是已经调用了wait(),并且释放了锁,正在等待通知的状态。 - “销毁前发出通知”的真正含义:这里的“销毁前”是指调用析构函数之前,而不是在析构函数内部执行通知。标准明确要求:一旦析构函数启动,就不能有线程还在等待这个条件变量。
换句话说,你必须在触发析构(比如delete)之前,先确保所有等待线程都被通知并离开等待队列,而不是把通知放在析构函数里“亡羊补牢”。
二、你的代码里的竞态条件
当定义NOTIFY_IN_DESTRUCTOR时,主线程的流程是:
- 等待
kill变为true(子线程设置) - 调用
delete nod,触发析构函数,在析构里调用cv.notify_all() - 等待子线程结束
这里存在两个致命的竞态:
1. 析构启动时,线程可能还在等待队列中
当主线程拿到flag锁并看到kill=true时,子线程刚释放了flag锁(因为wait()会自动释放锁),但此时子线程可能刚进入condition_variable的等待队列。这时候你触发析构,析构函数开始执行,但子线程还在等待队列里——直接违反了标准中“析构时不得有线程阻塞于*this”的要求。
虽然你在析构里调用了notify_all(),但此时条件变量的销毁流程已经启动,线程被唤醒后需要从等待队列中移除自己,而这个操作会访问已经开始销毁的条件变量,导致未定义行为(随机挂起就是典型表现)。
2. 子线程可能在条件变量销毁后才调用wait()
理论上存在一个极小的窗口:子线程设置kill=true后,还没来得及调用cv.wait(),主线程就已经完成了delete nod(虽然因为flag锁的存在,这个窗口非常小,但循环10000次后总会触发)。此时子线程调用nod->cv.wait(),访问的是已经被析构的对象,直接导致未定义行为。
三、为什么未定义宏时能稳定运行?
当你不在析构里通知时,流程变成:
- 主线程拿到
flag锁,确认kill=true - 调用
cv.notify_all()(此时子线程已经在等待队列中,因为主线程能拿到锁的前提是子线程已经释放锁并进入wait()) - 子线程被唤醒,离开等待队列,重新获取锁后退出
wait() - 主线程调用
delete nod,此时条件变量没有任何线程在等待,析构完全安全
这个流程完全符合标准要求,所以不会出现挂起。
四、正确的做法
如果你想在对象销毁时通知等待线程,应该在析构之前完成通知,并且确保所有等待线程都已经离开等待队列。比如可以调整代码:
struct notify_on_delete { std::condition_variable cv; // 先调用这个函数完成通知,再销毁对象 void prepare_destroy() { cv.notify_all(); } ~notify_on_delete() { // 此时已经没有线程在等待了 } }; // 在main中: // ... nod->prepare_destroy(); run.join(); // 先join再delete,彻底避免竞态 delete nod; // ...
核心原则是:销毁condition_variable前,必须确保没有任何线程在等待它,也不会有线程后续再调用它的等待接口。
内容的提问来源于stack exchange,提问作者AngelGabriel

