多线程-伪唤醒机制

一、什么是伪唤醒

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

        // 否则继续等待
    }
}
相关推荐
葫芦和十三14 小时前
图解 MongoDB 28|块与迁移:balancer 怎么均衡数据
后端
葫芦和十三14 小时前
图解 MongoDB 27|分片策略:范围分片 vs 哈希分片
后端·mongodb·agent
paopaokaka_luck15 小时前
英语单词学习系统的设计与实现(基于艾宾浩斯遗忘曲线的智能复习机制、语音朗读、多题型测试与错题自动收录、Echarts图形化分析)
vue.js·spring boot·后端·echarts
我叫黑大帅15 小时前
MySQL 并发插入竞态问题:原子写入实践指南
后端·mysql·面试
zhangxingchao16 小时前
AI大模型核心八:从 Agent Skill、长文档 RAG 到知识库更新与训练策略
前端·人工智能·后端
我叫黑大帅17 小时前
别再手动写 updated_at了,这坑我踩过
后端·面试·go
zhangxingchao17 小时前
AI大模型核心七:从 Workflow、RAG、记忆治理到幂等性
前端·人工智能·后端
犀利豆18 小时前
AI in Harness(二)
java·人工智能·后端
IT_陈寒18 小时前
Python装饰器竟然偷偷改了函数名?这个坑我爬了三天
前端·人工智能·后端
bkl_921319 小时前
GPT-Image-2 文生图:如何稳定生成完整的人物全身照?
后端·gpt·restful