3、线程间通信机制:Event、Condition、Queue的原理与选择

多线程编程里,线程间怎么“说话”,是个绕不开的话题。说白了,就是怎么让一个线程知道另一个线程干完了活,或者怎么把数据安全地递过去。

我这些年做高并发系统,见过太多因为通信机制选错而翻车的案例。有人用错了 Condition,导致死锁;有人滥用 Event,性能一塌糊涂。今天咱们就把 Event、Condition、Queue 这三兄弟掰扯清楚。

3.1 Event:最简单的“信号灯”

Event 本质上就是个布尔标志位。一个线程等它亮,另一个线程把它点亮。就这么简单。

我习惯把它用在“一次性通知”的场景。比如服务启动时,主线程等所有子线程初始化完毕,再继续往下走。

核心方法:
  • set() — 把标志置为 True,唤醒所有等待的线程
  • clear() — 把标志置为 False
  • wait() — 阻塞,直到标志为 True
  • is_set() — 检查当前状态
import threading

# 模拟:主线程等所有 worker 就绪
ready_event = threading.Event()

def worker(worker_id):
    # 模拟初始化
    print(f"Worker {worker_id} 初始化中...")
    time.sleep(1)
    print(f"Worker {worker_id} 就绪")
    ready_event.set()  # 点亮信号灯

# 主线程
ready_event.wait()  # 阻塞,直到有 worker 点亮
print("所有 worker 就绪,开始干活")
我的经验:Event 适合“一次性”场景。如果你需要反复通知,比如生产者每生产一个就通知消费者,那 Event 就不太合适了。因为 set() 之后如果不 clear(),wait() 会直接通过,起不到“等待”效果。

3.2 Condition:更精细的“等待-通知”

Condition 比 Event 高级一点。它允许线程在满足某个条件时等待,另一个线程在条件变化时通知它。

嗯,这里要注意:Condition 必须和锁(Lock)配合使用。它内部维护了一个锁,你调用 wait() 时会释放锁,被唤醒后又会重新获取锁。

我曾经在一个资源池项目里用过 Condition。多个线程竞争有限的连接,没有连接时就等着,有人归还了就通知一个线程去抢。

import threading

class ConnectionPool:
    def __init__(self, size):
        self._pool = [f"conn-{i}" for i in range(size)]
        self._cond = threading.Condition()

    def acquire(self):
        with self._cond:
            while not self._pool:  # 用 while 不是 if,防止虚假唤醒
                self._cond.wait()
            return self._pool.pop()

    def release(self, conn):
        with self._cond:
            self._pool.append(conn)
            self._cond.notify()  # 通知一个等待的线程
避坑指南:我曾经在 wait() 外面用了 if 而不是 while,结果遇到虚假唤醒(spurious wakeup),线程拿到一个已经被别人抢走的连接,直接崩溃。记住:wait() 一定要放在 while 循环里。

Condition 的 notify() 只唤醒一个线程,notify_all() 唤醒所有。我个人习惯:如果不知道唤醒谁合适,就用 notify_all(),虽然有点浪费,但安全。

3.3 Queue:生产者的“传送带”

Queue 是我用得最多的通信方式。它把同步的细节都封装好了,你只管往里放、往外取就行。

说白了,Queue 就是“生产者-消费者”模式的完美实现。它内部用了 Condition 和锁,但对外暴露的是简单的 put() 和 get()。

方法 说明 阻塞行为
put(item, block=True, timeout=None) 放入数据 队列满时阻塞,直到有空位或超时
get(block=True, timeout=None) 取出数据 队列空时阻塞,直到有数据或超时
task_done() 标记任务完成 配合 join() 使用,统计未完成任务数
join() 等待所有任务完成 阻塞直到 task_done() 调用次数等于 put 次数
import threading
import queue
import time

q = queue.Queue(maxsize=10)

def producer():
    for i in range(20):
        q.put(f"data-{i}")
        print(f"生产: data-{i}")
        time.sleep(0.1)

def consumer():
    while True:
        data = q.get()
        print(f"消费: {data}")
        q.task_done()  # 标记处理完成

# 启动消费者线程(设为守护线程,主线程结束它就结束)
t = threading.Thread(target=consumer, daemon=True)
t.start()

producer()
q.join()  # 等待所有数据被消费
print("所有任务完成")
我的建议:能用 Queue 就别自己造轮子。它支持线程安全、阻塞超时、优先级队列(PriorityQueue)、后进先出(LifoQueue)。我做过一个日志收集系统,就用 PriorityQueue 让高优先级的错误日志优先处理。

3.4 怎么选?一张图说清楚

你想想看,这三个东西各有各的脾气。选错了,轻则性能差,重则死锁。我总结了一个决策流程:

需要线程间通信? 是“一次性” 通知? 是 Event 否 需要传递 数据? 是 Queue 否 Condition 选择依据:是否需要传递数据?是否重复通知?

3.5 实战对比:三种场景怎么选

场景 推荐方案 原因
线程初始化完成通知 Event 一次性通知,不需要传递数据,简单高效
资源池(连接池、线程池) Condition 需要精确控制等待和通知,资源不足时等待,归还时通知
生产者-消费者(任务队列) Queue 需要传递数据,且可能有多个生产者和消费者
多阶段流水线处理 多个 Queue 串联 每个阶段一个队列,解耦且易于扩展
超时等待某个条件 Condition + wait(timeout) Event 的 wait() 也支持超时,但 Condition 更灵活
我曾经踩过的坑:在一个高并发网关里,我用 Event 做“请求完成通知”,结果因为 Event 是“一次性”的,第二次请求时 wait() 直接返回,导致逻辑错乱。后来改成 Condition + 计数器才解决问题。所以,一定要想清楚你的场景是“一次性”还是“重复性”。

3.6 总结一下

Event 就是个开关,适合“喊一嗓子”的场景。Condition 像个会议室,大家排队等条件满足。Queue 就是传送带,数据从一头进另一头出。

我个人习惯:能上 Queue 就上 Queue,它封装得最好,不容易出错。如果只是简单通知,用 Event。需要精细控制等待条件,才用 Condition。

记住一点:线程间通信,安全第一,性能第二。别为了省几个微秒的锁开销,搞出死锁或者数据不一致,那才是真正的灾难。

可可精品资料库