4. 线程间通信:Condition条件变量、Event事件对象、Queue队列的使用

多线程编程里,线程之间怎么「说话」,是个绕不开的问题。

我刚开始写多线程代码时,总觉得每个线程各干各的就行。后来发现,线程之间不沟通,程序就像一群人在黑屋子里乱撞——效率低,还容易出事。

Python 给了我们三种好用的通信工具:ConditionEventQueue。它们各有各的脾气,用对了地方,代码会清爽很多。

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% 的线程通信需求。记住一点:线程通信的核心不是「怎么传数据」,而是「怎么协调好谁先谁后」。选对了工具,代码写起来顺手,跑起来也稳当。