4、线程间通信:Condition条件变量、Event事件、Queue队列
线程间通信,说白了就是多个线程之间怎么「打招呼」。
我刚开始写多线程代码时,总觉得线程之间各干各的就行。后来线上出了个bug,两个线程同时修改一个共享变量,数据全乱了。嗯,从那以后我才真正重视起线程通信来。
Python里常用的通信工具有三个:Condition、Event、Queue。它们各有各的适用场景,用错了反而麻烦。
4.1 Condition:条件变量
Condition,我习惯叫它「条件变量」。它解决的是这样一个问题:一个线程在等某个条件成立,另一个线程负责把条件「搞成立」。
举个例子。生产者消费者模型里,消费者要等队列里有东西才能取。如果队列为空,消费者就得等着。生产者往队列里放东西后,得通知消费者「别等了,有货了」。
Condition就是干这个的。
核心机制:
acquire()/release():获取和释放底层锁wait():释放锁并阻塞,直到被通知notify()/notify_all():唤醒一个或所有等待的线程
看个代码你就明白了:
import threading
import time
class MessageQueue:
def __init__(self):
self.items = []
self.cond = threading.Condition()
def put(self, item):
with self.cond:
self.items.append(item)
print(f"生产了: {item}")
self.cond.notify() # 通知消费者
def get(self):
with self.cond:
while not self.items: # 注意这里是while,不是if
self.cond.wait() # 没东西就等着
item = self.items.pop(0)
print(f"消费了: {item}")
return item
def producer(q):
for i in range(5):
q.put(i)
time.sleep(0.5)
def consumer(q):
for _ in range(5):
q.get()
q = MessageQueue()
t1 = threading.Thread(target=producer, args=(q,))
t2 = threading.Thread(target=consumer, args=(q,))
t1.start()
t2.start()
t1.join()
t2.join()
我曾经踩过的坑:
wait() 一定要放在 while 循环里,不能用 if。为什么?因为线程被唤醒后,条件可能又被别的线程改了。用 while 可以重新检查条件,避免「虚假唤醒」。这个坑我调试了整整一个下午才找到原因。
4.2 Event:事件
Event比Condition简单得多。它就是一个「开关」:要么开(True),要么关(False)。
我个人的习惯是:当只需要「通知一次」或者「广播通知」时,用Event最合适。比如程序启动时,主线程等所有子线程初始化完成再继续。
import threading
import time
event = threading.Event()
def worker(name):
print(f"线程 {name} 正在初始化...")
time.sleep(2)
print(f"线程 {name} 初始化完成")
event.set() # 设置事件
def boss():
print("老板线程等待所有工人初始化...")
event.wait() # 等待事件被设置
print("所有工人就绪,开始干活!")
t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=boss)
t1.start()
t2.start()
t1.join()
t2.join()
Event的三个核心方法:
set():把标志设为True,唤醒所有等待的线程clear():把标志设为Falsewait():阻塞直到标志为True
小技巧:
Event适合「一次性」的通知场景。如果你需要多次通知(比如生产者不断生产),用Condition或Queue更合适。Event的clear()方法虽然可以重置,但用起来容易出问题,我一般不建议这么用。
4.3 Queue:队列
Queue是我用得最多的线程通信工具。说白了,它就是线程安全的队列,内部已经帮你处理好了锁和条件变量。
你想想看,如果每次写生产者消费者都要自己实现Condition,多累啊。Queue直接封装好了,拿来就用。
import threading
import queue
import time
q = queue.Queue(maxsize=3)
def producer():
for i in range(5):
q.put(i)
print(f"生产: {i}, 队列大小: {q.qsize()}")
time.sleep(0.3)
def consumer():
while True:
item = q.get()
print(f"消费: {item}, 队列大小: {q.qsize()}")
q.task_done() # 通知队列任务完成
time.sleep(0.5)
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer, daemon=True)
t1.start()
t2.start()
t1.join()
q.join() # 等待所有任务被处理完
Queue的常用方法:
| 方法 | 说明 | 阻塞行为 |
|---|---|---|
put(item, block=True, timeout=None) |
放入元素 | 队列满时阻塞 |
get(block=True, timeout=None) |
取出元素 | 队列空时阻塞 |
task_done() |
标记任务完成 | 配合join()使用 |
join() |
等待所有任务完成 | 阻塞直到task_done调用次数等于put次数 |
qsize() |
返回队列大小 | 非阻塞 |
三种队列类型:
queue.Queue:先进先出(FIFO),最常用queue.LifoQueue:后进先出(LIFO),类似栈queue.PriorityQueue:优先级队列,优先级低的先出
4.4 如何选择?
我总结了一个简单的选择标准:
- 需要多次通信、条件复杂 → 用Condition。比如多个消费者等待不同条件。
- 只需要通知一次、广播通知 → 用Event。比如所有线程同时开始。
- 生产者消费者模型、数据传递 → 用Queue。90%的场景都够用。
注意:
Queue虽然方便,但别滥用。如果只是传递简单的信号(比如「停止工作」),用Event更轻量。Queue内部用了锁,频繁的put/get会有性能开销。我在一个高并发项目里就吃过这个亏,后来换成Event+共享变量,性能提升了30%。
4.5 知识体系图
下面这张图帮你理清这三个工具的关系和适用场景:
嗯,这张图基本把三个工具的核心区别说清楚了。我个人建议:能用Queue就别自己造轮子。除非你有特殊需求,否则Queue是最省心的选择。
我的经验之谈:
实际项目中,我80%的场景用Queue,15%用Event,只有5%用Condition。Condition虽然功能强大,但用起来容易出错。如果你发现代码里Condition的逻辑越来越复杂,不妨想想能不能用Queue+Event的组合替代。
好了,线程间通信就聊到这儿。记住一句话:多线程的难点不在语法,而在设计。选对工具,代码自然就清晰了。