Skip to content

Linux下WaitGroup概率出信号丢失导致永久阻塞 #146

Description

@helinljn
  1. 今天跑测试用例无意中发现的,懒得自己写,所以AI味儿很浓,勿怪
  2. 环境:WSL + Ubuntu 24.04 + gcc 13.3.0 + C++17(不确定是否跟WSL环境有关系)
// WaitGroup.hpp
void done()
{
    --mCounter;           // ① 原子递减(无锁)
    mCond.notify_all();   // ② 通知(无锁)
}

void wait()
{
    std::unique_lock<std::mutex> l(mMutex);  // ③ 加锁
    mCond.wait(l, [&] {                       // ④ 等待谓词
        return mCounter <= 0;
    });
}

具体逻辑路径(竞态时序)

时间线    主线程 (wait)                   工作线程 (done)
──────    ─────────────                   ──────────────
  T1      wg->add(2);  // mCounter=2
  T2      启动 t1, t2
  T3      wg->wait()
  T4        l(mMutex)  ← 获取 mMutex
  T5        谓词检查: mCounter=2 > 0, 不满足
  T6                                        --mCounter;  // mCounter=1
  T7                                        mCond.notify_all();  ← 关键!
  T8                                        --mCounter;  // mCounter=0
  T9                                        mCond.notify_all();
 T10        mCond.wait()  ← 进入等待...

关键问题在 T7/T9 时刻notify_all() 被调用时,主线程尚未真正进入 mCond.wait() 的内核等待队列。condition_variable::wait() 的内部执行分为三步:

  1. 释放 mMutex(unlock)
  2. 将自己加入等待队列(原子操作)
  3. 进入阻塞

只有步骤 2 完成后,notify_all() 才能唤醒等待线程。如果 notify_all() 在步骤 1 和步骤 2 之间发生(即主线程已释放 mMutex 但还未进入等待队列),这个通知就会丢失

为什么在 Linux 上更容易出现

  1. pthread_cond_wait 的实现:glibc 的 pthread_cond_wait 在释放 mutex 后到进入内核等待队列之间有一个窗口期,在此期间 signal 会丢失。
  2. 线程调度差异:Linux 的调度器可能让工作线程在主线程释放 mutex 后、进入等待前迅速执行 done()
  3. Windows 上更难触发:Windows 的 CONDITION_VARIABLE 实现中,SleepConditionVariableCS 在释放临界区前就已将线程加入等待队列,缩小了竞态窗口。

次要问题:mCounteratomic<int> 但未与 mMutex 同步

std::mutex mMutex;
std::atomic<int> mCounter;   // ← 用了 atomic,但 wait() 中的谓词检查在 mutex 保护下
std::condition_variable mCond;

add() 完全无锁,done() 也无锁。这意味着:

  • wait() 持有 mMutex 检查 mCounter,但 done() 不持有 mMutex 就修改 mCounter 并调用 notify_all()
  • 条件变量的标准用法要求:修改共享状态和 notify 必须在同一个 mutex 保护下进行,或者至少保证 notify 不会在 wait 释放锁和进入等待之间丢失

修复方案

done() 应当在持锁状态下修改计数器并通知:

void done()
{
    std::lock_guard<std::mutex> l(mMutex);
    --mCounter;
    mCond.notify_all();
}

add() 同样需要持锁(若 add 在 wait 之后调用,计数器会从 0 变为正数,但 wait 已经返回了):

void add(int i = 1)
{
    std::lock_guard<std::mutex> l(mMutex);
    mCounter += i;
}

总结

项目 说明
阻塞类型 信号丢失导致的永久阻塞
根本原因 done() 无锁修改 mCounternotify_all(),而 wait() 在释放 mMutex 和进入条件变量等待之间存在竞态窗口,notify_all() 在此窗口内被调用会丢失
关键时序 主线程 wait() 释放 mMutex → 工作线程 done() 递减 + notify → 主线程进入等待(信号已丢失)
Linux 更易触发 glibc pthread_cond_wait 的 unlock→enqueue 非原子窗口较 Windows 更大
修复 done()add() 必须在 mMutex 保护下操作,确保 notify 与 wait 的谓词检查互斥

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions