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

线程间通信,说白了就是让多个线程能「好好说话」。

我刚开始做并发编程时,总觉得线程之间各干各的就行。后来线上出了个诡异的Bug——两个线程同时修改共享数据,结果数据全乱了。嗯,从那以后我才明白:线程之间必须有一套成熟的通信机制。

Python里提供了四种核心工具:Condition、Event、Queue、Semaphore。咱们一个一个聊。

4.1 Condition:条件变量

Condition,我习惯叫它「条件变量」。它解决的是这样一个场景:一个线程在等某个条件成立,另一个线程负责把条件「搞成立」。

核心思想:线程A等待条件 → 线程B满足条件并通知 → 线程A继续执行

举个例子。生产者-消费者模型里,消费者必须等生产者生产了东西才能消费。用Condition实现就很自然:

import threading
import time

class MessageQueue:
    def __init__(self):
        self.cond = threading.Condition()
        self.msg = None

    def produce(self, msg):
        with self.cond:
            while self.msg is not None:
                self.cond.wait()  # 等待消费者取走消息
            self.msg = msg
            print(f"生产: {msg}")
            self.cond.notify()  # 通知消费者

    def consume(self):
        with self.cond:
            while self.msg is None:
                self.cond.wait()  # 等待生产者生产
            result = self.msg
            self.msg = None
            print(f"消费: {result}")
            self.cond.notify()  # 通知生产者
            return result

个人经验:wait() 一定要放在 while 循环里,不要用 if。我在项目中遇到过虚假唤醒(spurious wakeup),用 if 的话线程被唤醒后条件可能还没满足,直接往下走就出错了。

4.2 Event:事件对象

Event 比 Condition 更轻量。它就是一个「开关」——要么开(set),要么关(clear)。

你想想看,如果只是通知「可以开始了」,不需要传递数据,用 Event 就够。我经常用它来做「启动信号」:

import threading
import time

start_event = threading.Event()

def worker(name):
    print(f"{name} 等待开始信号...")
    start_event.wait()  # 阻塞,直到事件被设置
    print(f"{name} 开始工作!")

# 启动多个工作线程
threads = [threading.Thread(target=worker, args=(f"Worker-{i}",)) for i in range(3)]
for t in threads:
    t.start()

time.sleep(2)
print("主线程发出开始信号")
start_event.set()  # 所有等待的线程同时被唤醒

注意:Event 是一次性的。set() 之后所有 wait() 都会通过。如果想重置,需要手动 clear()。但 clear() 之后新来的 wait() 又会阻塞。

4.3 Queue:队列通信

Queue 是我个人最推荐的线程间通信方式。它把「数据传递」和「线程安全」打包在一起,你几乎不需要自己加锁。

Python 的 queue.Queue 内部已经实现了 Condition 和锁。你只管 put 和 get 就行:

import threading
import queue
import time

q = queue.Queue(maxsize=5)

def producer():
    for i in range(10):
        q.put(i)
        print(f"生产: {i}")
        time.sleep(0.5)

def consumer():
    while True:
        item = q.get()
        print(f"消费: {item}")
        q.task_done()  # 告诉队列任务完成

# 启动生产者和消费者
threading.Thread(target=producer, daemon=True).start()
threading.Thread(target=consumer, daemon=True).start()

# 等待所有任务完成
q.join()
print("所有任务处理完毕")

为什么推荐 Queue?

  • 自动处理锁和条件变量
  • 支持阻塞和非阻塞操作
  • 可以设置最大容量,防止内存溢出
  • join() 和 task_done() 配合,能精确控制任务完成

我在项目中遇到过一个问题:生产者生产太快,消费者处理太慢。如果不限制队列大小,内存会一直涨。用 maxsize 限制后,put() 会自动阻塞,完美解决了背压问题。

4.4 Semaphore:信号量

Semaphore,说白了就是一个「计数器锁」。它控制同时访问某个资源的线程数量。

举个例子。数据库连接池只有5个连接,同时只能有5个线程使用。用 Semaphore 就很合适:

import threading
import time

# 最多允许3个线程同时访问
sem = threading.Semaphore(3)

def access_db(thread_id):
    with sem:  # 获取信号量,如果已满则阻塞
        print(f"线程 {thread_id} 正在访问数据库...")
        time.sleep(2)
        print(f"线程 {thread_id} 释放数据库连接")

# 启动10个线程
threads = [threading.Thread(target=access_db, args=(i,)) for i in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()

避坑指南:我曾经在项目里用 Semaphore 控制并发请求数,结果忘了 release(),导致信号量越来越少,最后所有线程都卡在 acquire() 上。用 with 语句可以避免这个问题,它会自动 release()。

4.5 四种机制对比

机制 适用场景 特点 我常用的地方
Condition 复杂的条件等待 需要手动 wait/notify 生产者-消费者(需要精确控制时)
Event 一次性信号通知 轻量,适合「开始/停止」信号 线程启动同步、服务关闭通知
Queue 数据传递 自带线程安全,最推荐 任务队列、日志收集
Semaphore 限制并发数 计数器锁,控制资源访问 连接池、限流

4.6 知识体系图

下面这张图帮你理清这四种机制的关系和选择逻辑:

线程间通信机制选择指南 线程间通信 Condition 条件变量 Event 事件对象 Queue 队列 Semaphore 信号量 复杂条件等待 需要精确控制 一次性信号 开始/停止通知 数据传递 最推荐使用 限制并发数 资源访问控制 我的建议 能用 Queue 就别自己造轮子 需要精确控制时用 Condition 简单信号用 Event,限流用 Semaphore

嗯,这四种机制基本覆盖了日常开发中90%的线程间通信场景。我个人最常用的是 Queue,因为它最省心。但遇到需要精细控制的情况,Condition 和 Semaphore 也各有千秋。

核心原则:选择最简单的方案。能用 Queue 就别用 Condition,能用 Event 就别自己写锁。代码越简单,越不容易出 Bug。

想要更多资料,联系可可