多线程-伪唤醒机制

一、什么是伪唤醒

它指一个正在条件变量(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;

        // 否则继续等待
    }
}
相关推荐
paopaokaka_luck9 分钟前
基于SpringBoot+Vue的电影售票系统(协同过滤算法)
vue.js·spring boot·后端
IT_10246 小时前
Spring Boot项目开发实战销售管理系统——系统设计!
大数据·spring boot·后端
ai小鬼头7 小时前
AIStarter最新版怎么卸载AI项目?一键删除操作指南(附路径设置技巧)
前端·后端·github
Touper.7 小时前
SpringBoot -- 自动配置原理
java·spring boot·后端
一只叫煤球的猫8 小时前
普通程序员,从开发到管理岗,为什么我越升职越痛苦?
前端·后端·全栈
一只鹿鹿鹿8 小时前
信息化项目验收,软件工程评审和检查表单
大数据·人工智能·后端·智慧城市·软件工程
专注VB编程开发20年8 小时前
开机自动后台运行,在Windows服务中托管ASP.NET Core
windows·后端·asp.net
程序员岳焱8 小时前
Java 与 MySQL 性能优化:MySQL全文检索查询优化实践
后端·mysql·性能优化
一只叫煤球的猫9 小时前
手撕@Transactional!别再问事务为什么失效了!Spring-tx源码全面解析!
后端·spring·面试
旷世奇才李先生9 小时前
Ruby 安装使用教程
开发语言·后端·ruby