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 适合那种「一锤子买卖」的通知。

最后说一句:多线程通信的核心不是工具本身,而是对「共享资源」的理解。不管用哪种方式,都要想清楚:哪些数据是共享的?什么时候需要同步?死锁怎么避免?想明白了,工具只是顺手的事。

线程间通信方式对比 Condition 条件变量 + 锁 • wait() - 等待条件 • notify() - 通知一个 • notify_all() - 通知全部 适用:复杂条件等待 注意:虚假唤醒 需配合 Lock 使用 Event 信号灯机制 • set() - 设置标志 • clear() - 清除标志 • wait() - 等待标志 适用:一次性通知 特点:简单粗暴 注意:不可重复使用 Queue 线程安全队列 • put() - 放入数据 • get() - 取出数据 • task_done() - 标记完成 适用:生产者-消费者 特点:最省心 推荐:90%场景用它 选择建议:复杂条件用 Condition,简单信号用 Event,数据传递用 Queue

小提示:实际项目中,我经常把 Queue 和 Event 结合起来用。Queue 负责数据传递,Event 负责控制流程。比如用 Event 通知消费者「有新数据了」,消费者再去 Queue 里取。这样既灵活又清晰。

可可精品资料库