Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

等待与通知

复习

  • 临界区与原子操作:哪些代码必须表现得像不可分割的一步
  • 互斥锁:让同一时刻只有一个执行线进入临界区
  • 自旋锁与睡眠锁:等待锁时应该原地重试还是让出 CPU

TL;DR

  • 光有锁还不够:有时线程要等“某个条件”成立,而条件不成立就不能往下走
  • 条件变量:让线程在条件不满足时睡下,条件满足时被唤醒
  • 它必须和锁配合使用,否则可能“错过唤醒”
  • 被唤醒后通常要用循环重新检查条件

正文

  锁解决了“同一时刻只能一个进”的问题。可还有一类等待,锁管不了:线程要等到某个条件成立,才能继续。

锁住也办不到的事

  想象生产者—消费者里的消费者:它要取走数据,可缓冲区是空的。这时它不能往下走,但也不能一直攥着锁等——那会让生产者连放数据的机会都没有。

  于是需求变成了:条件不满足时,先放开锁睡下;等条件满足了,再醒过来。

条件变量

  这个工具叫条件变量(condition variable)。它通常提供两个动作:

  • wait:睡下,同时释放锁,让别人能进来改变条件
  • signal:唤醒一个正在等待的线程

  典型用法是这样的:

lock()
while (条件不成立) {
    wait(锁)      // 睡下并放开锁;被唤醒后重新拿到锁
}
// 条件成立,办事
unlock()

  注意 wait 的一个巧妙之处:它一口气做了两件事——把当前线程睡下,同时把锁放开。这样,别的线程才进得来、把条件改成立,然后 signal 把你叫醒。

为什么要用循环

  被唤醒之后,为什么是用 while 重新检查条件,而不是 if

  因为被唤醒,不等于条件一定成立

  • 从你被唤醒到重新拿到锁,可能又有别人抢先改变了条件
  • 可能有多个线程被一次唤醒“惊”到,但只有一个人能如愿

  所以稳妥的做法是:醒来后再看一眼条件;不满足,就接着睡。这个“再看一眼”的循环,是条件变量的标准姿势。

容易踩的坑:错过唤醒

  条件变量必须和锁一起用,否则会出错。设想:你检查条件(不成立)→ 还没来得及睡下 → 另一个线程改变了条件并 signal → 你这时才睡下。

  结果,那次 signal 白白浪费,你睡下去却再也没人叫醒,程序卡住。这类问题叫错过唤醒。把“检查条件”和“睡下”放在同一把锁的保护里,就能堵住这个缝。

思考题

  如果被唤醒后只用 if 检查一次条件就往下走,可能出什么错?为什么 while 更安全?

小结

知识点

  • 条件变量用于等待“某个条件成立”
  • wait 睡下并释放锁,signal 唤醒等待者
  • 必须配合锁使用,避免错过唤醒
  • 唤醒后要用循环重新检查条件

参考资料

  1. Wikipedia(zh):条件变量:condition variable
  2. Wikipedia(zh):监视器:条件变量与锁的组合

思考题答案(仅供参考)

  用 if 检查一次就往下走,可能在你被唤醒、重新拿到锁的这段时间里,条件又被别的线程改回去了;或者一次唤醒惊动了多个线程,只有一个人满足条件,其他人却误以为成立。while 会在醒来后重新检查,条件不满足就继续睡,因此更安全。一句话:“被叫醒”只是提示,条件到底成不成立,还得自己再看一眼。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪