2. condition_variable 如何实现事件驱动

核心是:等待线程在内核里睡眠,不占用 CPU;通知线程把它唤醒。 等待期间,这个线程从调度器的运行队列里摘掉,完全不会被调度。条件满足后由通知方把它放回运行队列。这就是"事件驱动",与之相对的"轮询"是线程自己反复醒来检查条件。

方式 等待期间 CPU 响应延迟
忙等 while (!ready); 占满一个核 最低
睡眠轮询 while (!ready) sleep(10ms); 周期性醒来 最多多等一个轮询周期
condition_variable 0 一次唤醒的开销(系统调用加调度,微秒级)

用法

std::mutex m;
std::condition_variable cv;
bool ready = false;

// 等待方
std::unique_lock lk(m);
cv.wait(lk, [] { return ready; });     // 等价于 while (!ready) cv.wait(lk);

// 通知方
{ std::lock_guard lk(m); ready = true; }
cv.notify_one();

底层实现:Linux 上是 futex

libstdc++ 的 std::condition_variable 封装的是 pthread_cond_t,glibc 用 futex 实现它。futex 有两个操作:

  • futex_wait(addr, expected):内核在锁住等待队列后检查 *addr == expected相等才睡,不等就立即返回。
  • futex_wake(addr, n):唤醒在 addr 上睡眠的最多 n 个线程。

关键难点:唤醒丢失。 wait 要做两件事:释放 mutex,然后睡眠。如果通知恰好发生在这两步之间,等待方就会错过通知,永远睡下去。

futex 的"相等才睡"语义正好能解决这个问题。条件变量内部有一个序号,每次通知都会把它加一:

等待线程                                   通知线程
lock(m)
检查 ready == false
seq0 = cond.seq        ← 持锁时记下序号
unlock(m)
                                           lock(m); ready = true; unlock(m)
                                           cond.seq++; futex_wake(&cond.seq)  ← 这时还没人在睡
futex_wait(&cond.seq, seq0)
  内核检查:seq != seq0 → 立即返回,不睡   ← 通知没有丢
lock(m),再次检查 ready == true,wait 返回

序号在释放 mutex 之前就读好了。所以无论通知落在哪个时间点,要么等待方还没睡、futex_wait 发现序号变了直接返回,要么等待方已经睡着、被 futex_wake 叫醒。两种情况都不会丢通知。

内核这边:

  • futex_wait 把线程挂到按地址哈希的等待队列上,把线程状态设为睡眠,然后调用 schedule() 让出 CPU。
  • futex_wake 找到对应队列,把线程放回运行队列。

为什么必须用 mutex 加 while 循环

  • mutex:保护条件本身(ready),保证"检查条件然后睡"和"修改条件然后通知"这两组操作不会交错。
  • while 循环(或带谓词的 wait:被唤醒不代表条件一定成立,有两种情况:
    • 虚假唤醒:futex 可能因为信号等原因提前返回。
    • 被抢先:notify_one 唤醒了你,但在你重新拿到 mutex 之前,另一个线程已经把条件消费掉了。

实现细节

  • glibc 2.25 起的真实实现更复杂:用 64 位的 __wseq 和两组等待者(G1/G2),保证不会唤醒"通知之后才开始等待"的线程。核心仍然是上面的"比较后再睡"。
  • 没有等待者时,notify_one 只在用户态检查一下计数就返回,不进内核。
  • 在锁外 notify 可以避免一个问题:被唤醒的线程马上又因为拿不到 mutex 而阻塞。在锁内或锁外 notify 都是正确的。
  • C++20 的 std::atomic<T>::wait/notify 直接建立在 futex 上,比条件变量更轻,适合"等某个原子变量变化"的简单场景。

上面的 futex 调用序列和 glibc 实现细节来自源码知识,这次没有实测。要看实际的系统调用,可以在 dev 上 strace 一个生产者-消费者程序,能看到 futex(..., FUTEX_WAIT_BITSET_PRIVATE, ...)FUTEX_WAKE_PRIVATE 成对出现。