wait/notify 为什么必须配合 while?
学操作系统或写多线程程序时,很多人会先理解锁:同一时刻只允许一个线程修改共享数据。
但很快就会遇到另一个问题:如果条件还没满足,线程应该怎么办?
比如消费者线程想从任务队列里取任务,拿到锁后一看队列是空的。它继续占着锁没有意义,因为生产者还要进来放任务;它反复“加锁、检查、解锁”也不优雅,因为这会浪费 CPU。
这时候就需要条件变量。
1. 锁和条件变量分别解决什么问题?
一句话区分:
- 锁解决“谁能修改共享状态”
- 条件变量解决“条件不满足时,线程怎么等待”
条件变量本身不保存“任务来了”这种事实。真正的事实仍然在共享状态里,比如队列长度、结果数量、关闭标记等。
notify() 也不是把条件变成真的,它只是提醒等待线程:状态可能变了,你可以再检查一次。
2. wait() 到底做了什么?
一次正确的等待,大致包含这几步:
- 等待线程先拿到锁
- 检查共享状态
- 条件不满足,调用
wait() wait()原子地释放锁,并让线程进入等待- 其他线程修改共享状态后调用
notify() - 等待线程醒来,重新拿到锁,再检查条件
这里有两个容易踩坑的点。
第一,notify() 不会立刻释放锁。被唤醒的线程必须等通知者退出临界区,才能重新拿到锁继续执行。
第二,通知不会被条件变量“存档”。如果 notify() 发生时没有线程正在等待,它就是一次空通知。但只要共享状态已经被正确修改,后来的线程检查条件时会直接继续,不会错误地睡下去。
3. 为什么 wait() 必须配合 while?
很多初学者会写成这样:
with condition:
if not ready:
condition.wait()
这段代码的问题是:线程被唤醒,不代表条件一定成立。
原因至少有两个:
- 被唤醒后,其他线程可能先拿到锁并改变了状态
- 某些系统允许“伪唤醒”,线程可能在没有明确通知时返回
所以正确模式应该是:
with condition:
while not ready:
condition.wait()
在 Python 里,也可以直接使用 wait_for():
with condition:
condition.wait_for(lambda: ready)
wait_for(predicate) 本质上就是帮你封装了“等待、醒来、重新检查”的循环。
4. 一个实际例子:等待多个依赖全部就绪
假设一个接口要同时请求用户、订单和优惠服务。主线程要等三个依赖都完成后,才能继续组装页面。
这里的等待条件就是:results 的数量等于依赖数量。
import threading
import time
services = {
"优惠服务": 0.15,
"用户服务": 0.25,
"订单服务": 0.35,
}
results = {}
condition = threading.Condition()
def load_service(name, delay):
# 模拟网络请求,慢操作放在锁外
time.sleep(delay)
# 修改共享状态和发送通知,必须由同一把锁保护
with condition:
results[name] = "ok"
condition.notify()
threads = [
threading.Thread(target=load_service, args=(name, delay))
for name, delay in services.items()
]
for thread in threads:
thread.start()
with condition:
ready = condition.wait_for(
lambda: len(results) == len(services),
timeout=1.0,
)
snapshot = dict(results)
for thread in threads:
thread.join()
if ready:
print(f"依赖齐全,开始组装页面:{snapshot}")
else:
print(f"等待超时,当前已完成:{snapshot}")
一次可能的输出:
依赖齐全,开始组装页面:{'优惠服务': 'ok', '用户服务': 'ok', '订单服务': 'ok'}
这段代码里,真正表示“依赖是否齐全”的不是 notify(),而是 results 这份共享状态。
notify() 只是提醒主线程:有人写入结果了,你可以重新检查 len(results) == len(services) 是否成立。
5. notify() 和 notify_all() 怎么选?
一般可以这样理解:
notify():只需要唤醒一个等待者,比如一个新任务只够一个消费者处理notify_all():状态变化可能影响多个等待者,或者无法准确判断该唤醒谁
不过,唤醒越多不代表越快。
如果大量线程同时醒来争抢同一把锁,最后又因为条件不满足继续睡眠,反而会增加调度成本。所以 notify_all() 更像是正确性兜底,不应该无脑替代 notify()。
6. 写条件变量时记住这三条
条件变量的核心不是 API,而是共享状态、锁和通知之间的关系:
- 共享状态必须由锁保护
- 修改状态和发送通知要放在同一把锁内
- 等待条件必须用
while或wait_for()反复检查
如果只是线程间传递任务,优先考虑 queue.Queue。
如果只是通知某件事发生,优先考虑 threading.Event。
如果等待的是“资源数量”,可以考虑 threading.Semaphore。
Condition 更适合“多个线程围绕同一份共享状态,等待不同条件成立”的场景。
查看「计算机操作系统」系列

9660

被折叠的 条评论
为什么被折叠?



