在C++并发编程中,条件变量(std::condition_variable)是与互斥锁配合、实现线程间同步的重要工具。但在实际使用中,很多程序会出现明明没有调用notify、等待线程却从wait中返回的情况,进而读取到不符合预期的数据状态。要写出稳定的多线程代码,必须理解这种现象的来源以及标准库提供的应对机制。

什么是虚假唤醒
虚假唤醒(spurious wakeup)指的是:一个正在条件变量上等待的线程,在没有其他线程显式通知(notify_one或notify_all)、也没有发生超时的情况下,从等待函数中返回。这种行为在POSIX线程(pthread)以及基于其实现的C++标准库中是允许的,标准文档明确说明wait调用可能因为系统调度、信号中断等原因被唤醒。
从底层来看,操作系统在挂起线程时可能由于内核调度策略、硬件中断或者调试器附加等外部因素,使线程提前恢复运行。C++标准不保证每次wait返回都对应一次真实的通知,因此应用程序不能假设“wait返回就等于条件成立”。如果代码在wait返回后直接访问共享资源,就可能在一个并不满足前置条件的状态下进行修改,造成数据竞争或逻辑错误。
不带谓词检查的风险写法
下面是一段典型的错误示例:工作线程等待队列非空,但仅用wait阻塞,没有在唤醒后检查队列状态。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock); // 错误:没有谓词检查
int val = data_queue.front(); // 若虚假唤醒,队列可能为空
data_queue.pop();
std::cout << "got: " << val << std::endl;
}
上述代码在正常运行时看似没有问题,但一旦消费者线程遭遇虚假唤醒,data_queue.front()会在空队列上执行,导致未定义行为。此外,即便没有虚假唤醒,若多个消费者同时被notify_all唤醒,先取数据的线程会让后续线程面对空队列,同样会崩溃。
这种写法的根本缺陷是把“被唤醒”和“条件满足”画了等号。在多线程环境下,条件可能在通知发出后、本线程真正获取锁之前被其他线程改变,因此每次醒来都必须重新确认条件。
使用谓词检查的规范方案
std::condition_variable的wait成员函数提供了带谓词的重载:wait(lock, pred)。该重载等价于while (!pred()) { wait(lock); },即在内部循环检查条件,只有谓词返回true才会退出等待。这从语言层面消除了虚假唤醒带来的隐患。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
// 谓词检查:只有队列非空才退出wait
cv.wait(lock, [] { return !data_queue.empty(); });
int val = data_queue.front();
data_queue.pop();
std::cout << "got: " << val << std::endl;
}
在这段代码中,lambda表达式作为谓词传入wait。若线程被虚假唤醒,wait内部会发现队列仍为空,谓词返回false,于是自动继续等待。这保证了线程离开wait时,共享状态一定满足处理逻辑的前置要求。
如果使用的是不带谓词的wait,也可以手动写成循环形式,效果与谓词版本一致:
void consumer_manual() {
std::unique_lock<std::mutex> lock(mtx);
while (data_queue.empty()) {
cv.wait(lock);
}
int val = data_queue.front();
data_queue.pop();
std::cout << "got: " << val << std::endl;
}
手动循环的好处是逻辑直观,适合需要在等待前后插入日志或统计的场景;而谓词版本更简洁,且不容易遗漏循环结构。两者在正确性和性能上几乎没有差别,团队可根据代码规范统一选用。
notify选择对谓词模型的影响
在引入谓词后,通知端使用notify_one还是notify_all也需要重新考量。若只有一个等待线程且条件变为“有数据”,notify_one即可唤醒一个消费者,它检查谓词后正常处理。但如果多个消费者都在等待同一条件,且条件变为“队列非空”,使用notify_one可能只唤醒一个线程,其余线程继续睡,这通常是期望行为,能减少惊群效应。
当共享状态的变化可能让多个等待线程的条件同时成立(例如边界标志置位,所有等待者都应退出),则应使用notify_all,配合谓词让每个线程自行判断自己是否该继续。无论哪种通知方式,谓词都保证了线程不会在条件不成立时误操作,这是构建可靠并发模型的基础。
总结与实践建议
避免C++条件变量中虚假唤醒的核心原则只有一个:永远在wait返回后确认条件。优先使用带谓词的wait重载,或者手写while循环检查。不要依赖通知次数来推断状态,因为系统调度和其他线程的插足都会打破这种假设。
在真实项目中,建议把“条件变量加谓词”作为代码评审的硬性检查项,并为常见的等待场景封装工具函数,例如安全的等待队列、带超时的谓词等待等。这样既能防止个体开发者遗漏检查,也能让多线程模块的行为在整个系统中保持一致。