多线程-伪唤醒机制

一、什么是伪唤醒

它指一个正在条件变量(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 正确的使用

  1. 使用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;
        // ...
    }
  2. 使用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;

        // 否则继续等待
    }
}
相关推荐
asyncrustacean3 分钟前
有栈协程基本原理和实现
后端·rust·go
Chaney不会代码4 分钟前
Eino概述
后端
德育处主任28 分钟前
亚马逊云 Lambda 容器化部署教程
后端·容器
货拉拉技术36 分钟前
API灵活定义+极速驱动:货拉拉星图平台技术架构与优化实践
后端
写bug写bug44 分钟前
搞懂Spring Cloud Config配置信息自动更新原理
java·后端·架构
疯狂的程序猴1 小时前
跨平台团队如何组合工具完成iOS上架流程(含Appuploader实用环节)
后端
多客潇潇和多客逸逸是好朋友1 小时前
如何在代码里面更改数据库密码?
后端
该用户为高级用户1 小时前
从零构建高性能Web服务器:基于Nginx与Lua的实践指南
后端
iOS开发上架哦1 小时前
开发者视角的网络调试流程进化:抓包工具实践指南与Sniffmaster使用笔记
后端