4、线程间通信:Condition条件变量、Event事件对象、Queue队列的使用
多线程编程里,线程之间怎么「说话」,是个绕不开的话题。
我刚开始写多线程代码时,总觉得每个线程各干各的就行。后来发现,线程之间如果不沟通,程序就像一屋子人各说各话——乱成一锅粥。今天咱们就聊聊 Python 里三种常用的线程通信方式:Condition、Event 和 Queue。
4.1 Condition:让线程学会等待和通知
Condition 条件变量,说白了就是一把「带通知功能的锁」。它允许一个或多个线程等待某个条件成立,然后由其他线程来通知它们。
我在项目中遇到过这样一个场景:一个生产者线程往缓冲区里放数据,多个消费者线程从缓冲区取数据。缓冲区空了,消费者就得等着;缓冲区满了,生产者就得等着。用 Condition 来实现,代码会非常清晰。
核心要点:Condition 必须与 Lock 配合使用。它提供了 acquire()、release()、wait()、notify() 和 notify_all() 这几个方法。
import threading
import time
import random
class Buffer:
def __init__(self, size=5):
self.size = size
self.items = []
self.condition = threading.Condition()
def produce(self, item):
with self.condition:
while len(self.items) >= self.size:
print(f"缓冲区满了,生产者等待...")
self.condition.wait()
self.items.append(item)
print(f"生产了: {item},当前缓冲区: {self.items}")
self.condition.notify() # 通知消费者
def consume(self):
with self.condition:
while len(self.items) == 0:
print(f"缓冲区空了,消费者等待...")
self.condition.wait()
item = self.items.pop(0)
print(f"消费了: {item},当前缓冲区: {self.items}")
self.condition.notify() # 通知生产者
return item
避坑指南:我曾经犯过一个错误——用 if 而不是 while 来判断条件。wait() 返回后,条件可能已经被其他线程改变了。所以一定要用 while 循环重新检查条件,这叫「虚假唤醒」问题。
4.2 Event:简单粗暴的信号灯
Event 对象比 Condition 简单得多。它就像一个红绿灯——要么是绿灯(set),要么是红灯(clear)。线程看到红灯就等着,看到绿灯就通行。
Event 有三个核心方法:
- set():把标志设为 True,唤醒所有等待的线程
- clear():把标志设为 False
- wait():阻塞直到标志为 True
你想想看,什么时候用 Event 最合适?当一个线程需要等待另一个线程完成某个动作时。比如主线程等所有子线程初始化完毕再开始干活。
import threading
import time
def worker(event, worker_id):
print(f"工作线程 {worker_id} 正在初始化...")
time.sleep(2) # 模拟初始化
print(f"工作线程 {worker_id} 初始化完成")
event.set() # 通知主线程
def main():
event = threading.Event()
threads = []
for i in range(3):
t = threading.Thread(target=worker, args=(event, i))
t.start()
threads.append(t)
print("主线程等待所有工作线程初始化...")
event.wait() # 等待第一个线程完成
print("至少一个线程初始化完成,主线程继续执行")
for t in threads:
t.join()
注意:Event 是一次性的。set() 之后,所有 wait() 都会立即返回。如果你需要反复使用,记得在合适的时候调用 clear()。我个人习惯用 Event 做「一次性通知」,比如程序启动时的初始化同步。
4.3 Queue:最省心的线程安全队列
说实话,如果让我选一种最推荐的线程通信方式,我会选 Queue。它把锁、条件变量这些东西都封装好了,你拿来直接用就行。
Python 的 queue 模块提供了三种队列:
| 队列类型 | 特点 | 适用场景 |
|---|---|---|
| Queue(FIFO) | 先进先出 | 生产者-消费者模型 |
| LifoQueue(LIFO) | 后进先出 | 类似栈的场景 |
| PriorityQueue | 优先级队列 | 任务有优先级时 |
Queue 的核心方法:
- put(item, block=True, timeout=None):放入元素,队列满时阻塞
- get(block=True, timeout=None):取出元素,队列空时阻塞
- task_done():标记任务完成
- join():等待所有任务完成
import threading
import queue
import time
import random
def producer(q, name):
for i in range(5):
item = f"{name}-{i}"
q.put(item)
print(f"生产者 {name} 生产了: {item}")
time.sleep(random.random())
def consumer(q, name):
while True:
item = q.get()
if item is None: # 结束信号
break
print(f"消费者 {name} 消费了: {item}")
q.task_done()
time.sleep(random.random())
# 使用示例
q = queue.Queue(maxsize=3)
producers = [threading.Thread(target=producer, args=(q, f"P{i}")) for i in range(2)]
consumers = [threading.Thread(target=consumer, args=(q, f"C{i}")) for i in range(2)]
for t in producers + consumers:
t.start()
for t in producers:
t.join()
# 发送结束信号
for _ in consumers:
q.put(None)
for t in consumers:
t.join()
我的经验:用 Queue 时,记得用 None 作为「毒丸」来通知消费者退出。这是最优雅的停止方式。我曾经见过有人用全局变量来控制线程退出,结果各种竞态条件,调试到崩溃。
4.4 三种方式怎么选?
嗯,这里我总结一下我的选择标准:
- 需要复杂的条件等待(比如多个条件组合)→ 用 Condition
- 只需要简单的开关信号(比如初始化完成通知)→ 用 Event
- 生产者-消费者模型(数据传递)→ 用 Queue
说白了,90% 的场景用 Queue 就够了。Condition 虽然强大,但用起来容易出错。Event 适合那种「一锤子买卖」的通知。
最后说一句:多线程通信的核心不是工具本身,而是对「共享资源」的理解。不管用哪种方式,都要想清楚:哪些数据是共享的?什么时候需要同步?死锁怎么避免?想明白了,工具只是顺手的事。
小提示:实际项目中,我经常把 Queue 和 Event 结合起来用。Queue 负责数据传递,Event 负责控制流程。比如用 Event 通知消费者「有新数据了」,消费者再去 Queue 里取。这样既灵活又清晰。
可可精品资料库