4. 线程间通信:Condition条件变量、Event事件对象、Queue队列的使用
多线程编程里,线程之间怎么「说话」,是个绕不开的问题。
我刚开始写多线程代码时,总觉得每个线程各干各的就行。后来发现,线程之间不沟通,程序就像一群人在黑屋子里乱撞——效率低,还容易出事。
Python 给了我们三种好用的通信工具:Condition、Event 和 Queue。它们各有各的脾气,用对了地方,代码会清爽很多。
4.1 Condition条件变量:让线程学会「等待」和「通知」
Condition 说白了就是一把带「叫醒服务」的锁。
你想想看,有些场景下,一个线程必须等另一个线程完成某个操作后才能继续。比如生产者-消费者模型:消费者得等生产者生产了东西,才能去消费。
核心要点:Condition 总是和一把 Lock 绑定。它有两个关键动作——wait() 让线程等待,notify() 或 notify_all() 唤醒等待的线程。
我在项目中遇到过这样一个场景:一个爬虫程序,多个线程负责下载,一个线程负责解析。解析线程必须等下载线程把数据准备好。用 Condition 就特别合适。
import threading
import time
class DataBuffer:
def __init__(self):
self.data = None
self.cond = threading.Condition()
def produce(self, value):
with self.cond:
self.data = value
print(f"生产者: 生产了 {value}")
self.cond.notify() # 叫醒一个等待的消费者
def consume(self):
with self.cond:
while self.data is None:
print("消费者: 没数据,先等等...")
self.cond.wait() # 释放锁,进入等待
result = self.data
self.data = None
print(f"消费者: 消费了 {result}")
return result
buffer = DataBuffer()
def producer():
for i in range(3):
time.sleep(1)
buffer.produce(f"数据-{i}")
def consumer():
for _ in range(3):
result = buffer.consume()
print(f"拿到结果: {result}")
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
t1.join()
t2.join()
个人习惯:用 Condition 时,我总会在 wait() 外面套一个 while 循环。为什么?因为线程可能被「假唤醒」——明明条件没满足,却被叫醒了。用 while 再检查一次,更安全。
4.2 Event事件对象:一个简单的「信号旗」
Event 比 Condition 更轻量。它就像一个红绿灯:要么是红灯(未设置),要么是绿灯(已设置)。
线程可以等绿灯亮了再走,也可以自己把灯变红或变绿。
import threading
import time
event = threading.Event()
def waiter():
print("等待者: 我在等信号...")
event.wait() # 阻塞,直到 event 被 set
print("等待者: 信号来了!开始干活")
def setter():
time.sleep(2)
print("设置者: 好了,发信号!")
event.set()
t1 = threading.Thread(target=waiter)
t2 = threading.Thread(target=setter)
t1.start()
t2.start()
t1.join()
t2.join()
Event 的用法很直观:set() 设置标志,clear() 清除标志,wait() 等待标志被设置,is_set() 检查当前状态。
我曾经踩过的坑:Event 是一次性的。如果你调了 set() 之后又调 clear(),那之前已经通过 wait() 的线程不会受影响。但新来的线程会继续等待。所以,如果你需要反复使用同一个 Event,记得在合适的时候 clear()。
Event 适合什么场景?我常用它来做「启动信号」——所有线程都准备好了,主线程一声令下,大家一起开始跑。
4.3 Queue队列:最省心的线程间通信方式
说实话,我个人最推荐用 Queue。它把锁、条件变量这些东西都封装好了,你只管往里放数据、往外取数据就行。
Python 的 queue.Queue 是线程安全的。多个生产者可以同时往里放,多个消费者可以同时往外取,不会乱。
import threading
import queue
import time
q = queue.Queue(maxsize=5)
def producer(name):
for i in range(5):
item = f"{name}-产品-{i}"
q.put(item)
print(f"生产者 {name}: 放入 {item}")
time.sleep(0.5)
def consumer(name):
while True:
item = q.get()
if item is None: # 结束信号
break
print(f"消费者 {name}: 取出 {item}")
q.task_done() # 告诉队列,这个任务处理完了
time.sleep(1)
# 启动生产者
threads = []
for i in range(2):
t = threading.Thread(target=producer, args=(f"工厂{i}",))
threads.append(t)
t.start()
# 启动消费者
for i in range(3):
t = threading.Thread(target=consumer, args=(f"工人{i}",))
threads.append(t)
t.start()
# 等待所有生产完成
for t in threads[:2]:
t.join()
# 发送结束信号
for _ in range(3):
q.put(None)
# 等待消费者结束
for t in threads[2:]:
t.join()
Queue 的三个好兄弟:
queue.Queue— 先进先出,最常用queue.LifoQueue— 后进先出,像栈一样queue.PriorityQueue— 优先级队列,优先级高的先出
你想想看,用 Queue 的好处是什么?你不用自己管锁,不用管 Condition,不用管线程什么时候该等、什么时候该醒。Queue 全帮你搞定了。
4.4 三种方式怎么选?
| 通信方式 | 适用场景 | 复杂度 | 我的推荐指数 |
|---|---|---|---|
| Condition | 需要精确控制线程的等待/唤醒时机 | 高 | ⭐⭐⭐ |
| Event | 简单的信号通知,一次性或少量触发 | 低 | ⭐⭐⭐⭐ |
| Queue | 生产者-消费者模式,数据传递 | 极低 | ⭐⭐⭐⭐⭐ |
我的建议:能用 Queue 就别自己造轮子。它封装得好,不容易出错。Event 适合做「开关」或「信号」。Condition 嘛,除非你有特别复杂的等待条件,否则用 Queue 或 Event 就够了。
嗯,这三种方式基本覆盖了日常开发中 90% 的线程通信需求。记住一点:线程通信的核心不是「怎么传数据」,而是「怎么协调好谁先谁后」。选对了工具,代码写起来顺手,跑起来也稳当。