一、什么是伪唤醒
它指一个正在条件变量(Condition Variable)上等待的线程,在没有被任何其他线程显式通知(notify)的情况下,被意外地唤醒。
二、为什么会发生伪唤醒
伪唤醒的出现并非偶然,其背后有深刻的性能与设计权衡,原因通常涉及操作系统内核的实现:
- 性能优化 :在操作系统内核层面,要实现一个完全精确、杜绝任何意外唤醒的
wait/notify
机制,成本非常高昂。在某些复杂的竞态条件下,内核为了避免陷入复杂的判断逻辑,可能会选择唤醒一个或多个可能满足条件的线程,让线程自己在用户态进行二次确认。 - 竞态条件消除 :在
notify
信号发出和等待线程被唤醒之间存在时间差。为了解决这个时间差中可能出现的复杂竞态问题,一些内核实现选择了一个更简单的模型,即允许伪唤醒的存在。 - 系统信号中断 :在类UNIX系统中(如Linux),一个阻塞的系统调用(
wait
的底层实现)可能会被操作系统的信号(Signal)所中断,从而导致调用提前返回,表现为一次伪唤醒。
三、条件变量的使用
将条件变量的 wait
调用置于一个 while
循环中,并反复检查一个作为条件的谓词(Predicate)。 或者利用c++标准库提供的一个接受谓词wait的重载。 学习多线程并发相关的知识
3.1 错误案例
cpp
// 错误代码:没有使用 while 循环
std::unique_lock<std::mutex> lock(mtx);
if (!is_data_ready) { // if 只能检查一次
cv.wait(lock);
}
// 如果发生伪唤醒,线程会在这里继续执行,
// 此时 is_data_ready 依然是 false,导致逻辑错误。
process_data();
3.2 正确的使用
-
使用
while
不断检查cpp#include <mutex> #include <condition_variable> std::mutex mtx; std::condition_variable cv; bool is_data_ready = false; void consumer_thread() { // 1. 获取锁 std::unique_lock<std::mutex> lock(mtx); // 2. 在 while 循环中检查谓词 while (!is_data_ready) { // 3. 如果条件不满足,则调用 wait() // wait() 会原子地:(a) 释放锁 (b) 阻塞线程 cv.wait(lock); // 当线程被唤醒时(无论是正常通知还是伪唤醒), // 它会重新获取锁,然后再次检查 while 的条件。 } // 4. 只有当 is_data_ready 为 true 时,循环才会退出 // 此时线程仍然持有锁,可以安全地处理数据。 std::cout << "Data is ready, processing..." << std::endl; // ... }
-
使用c++标准库的
cpp// 等价的简洁写法 std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return is_data_ready; }); // 内部实现了 while 循环
3.3 模拟中断破坏
当我尝试在linux下去模拟线程被信号打断的时候:会发现并不能让被阻塞的线程被伪唤醒。
维基百科:To allow for implementation flexibility in dealing with error conditions and races inside the operating system, condition variables may also be allowed to return from a wait even if not signaled, though it is not clear how many implementations do that. In the Solaris implementation of condition variables, a spurious wakeup may occur without the condition being assigned if the process is signaled; the wait system call aborts and returns EINTR
. The Linux p-thread implementation of condition variables guarantees that it will not do that.
但是:这并不意味着Linux下没有伪唤醒
这只是帮我们排除了中断类型的唤醒,但是还有性能优化上面的唤醒。
四、其他同步操作
并非所有阻塞机制都有伪唤醒问题。例如 std::future::get()
,它等待一个一次性的结果。future
内部的共享状态 是单向且稳定的:一旦从"未就绪"变为"就绪",就再也不会改变。因此,get()
的返回必然意味着结果已经可用,它在设计上杜绝了伪唤醒的可能性。
std::future::get()
相关源码:
cpp
// 使用 Futex 等待直到满足条件或超时(可选)
unsigned _M_load_and_test_until(unsigned __assumed, unsigned __operand,
bool __equal, memory_order __mo,
bool __has_timeout,
chrono::seconds __s,
chrono::nanoseconds __ns)
{
for (;;)
{
// 标记自己为"等待者"
_M_data.fetch_or(_Waiter_bit, memory_order_relaxed);
// 进入内核等待(futex 阻塞)
bool __ret = _M_futex_wait_until(
(unsigned*)(void*)&_M_data,
__assumed | _Waiter_bit,
__has_timeout, __s, __ns
);
// 唤醒后重新读取状态值
__assumed = _M_load(__mo);
// 若唤醒后状态符合条件,则返回
if (!__ret || ((__operand == __assumed) == __equal))
return __assumed;
// 否则继续等待
}
}