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():把标志设为False
  • wait():阻塞直到标志为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 知识体系图

下面这张图帮你理清这三个工具的关系和适用场景:

线程间通信工具 Condition Event Queue 特性 • 需要显式获取锁 • wait/notify 机制 • 支持复杂条件判断 • 需配合 while 循环 特性 • 简单开关标志 • set/wait/clear • 一次性通知 • 广播唤醒所有 特性 • 线程安全队列 • 自动管理锁 • 支持阻塞超时 • 三种队列类型 适用:复杂条件、多次通信 如:多消费者等待不同条件 适用:一次性通知、广播 如:所有线程同时开始 适用:生产者消费者 如:任务队列、数据传递

嗯,这张图基本把三个工具的核心区别说清楚了。我个人建议:能用Queue就别自己造轮子。除非你有特殊需求,否则Queue是最省心的选择。

我的经验之谈:

实际项目中,我80%的场景用Queue,15%用Event,只有5%用Condition。Condition虽然功能强大,但用起来容易出错。如果你发现代码里Condition的逻辑越来越复杂,不妨想想能不能用Queue+Event的组合替代。

好了,线程间通信就聊到这儿。记住一句话:多线程的难点不在语法,而在设计。选对工具,代码自然就清晰了。

可可精品资料库