等待与通知
复习
- 临界区与原子操作:哪些代码必须表现得像不可分割的一步
- 互斥锁:让同一时刻只有一个执行线进入临界区
- 自旋锁与睡眠锁:等待锁时应该原地重试还是让出 CPU
TL;DR
- 光有锁还不够:有时线程要等“某个条件”成立,而条件不成立就不能往下走
- 条件变量:让线程在条件不满足时睡下,条件满足时被唤醒
- 它必须和锁配合使用,否则可能“错过唤醒”
- 被唤醒后通常要用循环重新检查条件
正文
锁解决了“同一时刻只能一个进”的问题。可还有一类等待,锁管不了:线程要等到某个条件成立,才能继续。
锁住也办不到的事
想象生产者—消费者里的消费者:它要取走数据,可缓冲区是空的。这时它不能往下走,但也不能一直攥着锁等——那会让生产者连放数据的机会都没有。
于是需求变成了:条件不满足时,先放开锁睡下;等条件满足了,再醒过来。
条件变量
这个工具叫条件变量(condition variable)。它通常提供两个动作:
wait:睡下,同时释放锁,让别人能进来改变条件signal:唤醒一个正在等待的线程
典型用法是这样的:
lock()
while (条件不成立) {
wait(锁) // 睡下并放开锁;被唤醒后重新拿到锁
}
// 条件成立,办事
unlock()
注意 wait 的一个巧妙之处:它一口气做了两件事——把当前线程睡下,同时把锁放开。这样,别的线程才进得来、把条件改成立,然后 signal 把你叫醒。
为什么要用循环
被唤醒之后,为什么是用 while 重新检查条件,而不是 if?
因为被唤醒,不等于条件一定成立:
- 从你被唤醒到重新拿到锁,可能又有别人抢先改变了条件
- 可能有多个线程被一次唤醒“惊”到,但只有一个人能如愿
所以稳妥的做法是:醒来后再看一眼条件;不满足,就接着睡。这个“再看一眼”的循环,是条件变量的标准姿势。
容易踩的坑:错过唤醒
条件变量必须和锁一起用,否则会出错。设想:你检查条件(不成立)→ 还没来得及睡下 → 另一个线程改变了条件并 signal → 你这时才睡下。
结果,那次 signal 白白浪费,你睡下去却再也没人叫醒,程序卡住。这类问题叫错过唤醒。把“检查条件”和“睡下”放在同一把锁的保护里,就能堵住这个缝。
思考题
如果被唤醒后只用
if检查一次条件就往下走,可能出什么错?为什么while更安全?
小结
知识点
- 条件变量用于等待“某个条件成立”
wait睡下并释放锁,signal唤醒等待者- 必须配合锁使用,避免错过唤醒
- 唤醒后要用循环重新检查条件
参考资料
- Wikipedia(zh):条件变量:condition variable
- Wikipedia(zh):监视器:条件变量与锁的组合
思考题答案(仅供参考)
用 if 检查一次就往下走,可能在你被唤醒、重新拿到锁的这段时间里,条件又被别的线程改回去了;或者一次唤醒惊动了多个线程,只有一个人满足条件,其他人却误以为成立。while 会在醒来后重新检查,条件不满足就继续睡,因此更安全。一句话:“被叫醒”只是提示,条件到底成不成立,还得自己再看一眼。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪