Python 条件变量 Condition 讲透:wait/notify 为什么必须配合 while?

wait/notify 为什么必须配合 while?

学操作系统或写多线程程序时,很多人会先理解锁:同一时刻只允许一个线程修改共享数据。

但很快就会遇到另一个问题:如果条件还没满足,线程应该怎么办?

比如消费者线程想从任务队列里取任务,拿到锁后一看队列是空的。它继续占着锁没有意义,因为生产者还要进来放任务;它反复“加锁、检查、解锁”也不优雅,因为这会浪费 CPU。

这时候就需要条件变量。

1. 锁和条件变量分别解决什么问题?

一句话区分:

  • 锁解决“谁能修改共享状态”
  • 条件变量解决“条件不满足时,线程怎么等待”

条件变量本身不保存“任务来了”这种事实。真正的事实仍然在共享状态里,比如队列长度、结果数量、关闭标记等。

notify() 也不是把条件变成真的,它只是提醒等待线程:状态可能变了,你可以再检查一次。

2. wait() 到底做了什么?

一次正确的等待,大致包含这几步:

  1. 等待线程先拿到锁
  2. 检查共享状态
  3. 条件不满足,调用 wait()
  4. wait() 原子地释放锁,并让线程进入等待
  5. 其他线程修改共享状态后调用 notify()
  6. 等待线程醒来,重新拿到锁,再检查条件

这里有两个容易踩坑的点。

第一,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,而是共享状态、锁和通知之间的关系:

  1. 共享状态必须由锁保护
  2. 修改状态和发送通知要放在同一把锁内
  3. 等待条件必须用 whilewait_for() 反复检查

如果只是线程间传递任务,优先考虑 queue.Queue

如果只是通知某件事发生,优先考虑 threading.Event

如果等待的是“资源数量”,可以考虑 threading.Semaphore

Condition 更适合“多个线程围绕同一份共享状态,等待不同条件成立”的场景。

查看「计算机操作系统」系列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗念笙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值