Python中的并发编程:线程同步与条件变量(Condition)详解
字数 1433 2025-12-10 13:39:58

Python中的并发编程:线程同步与条件变量(Condition)详解


1. 问题引入:为什么要用条件变量?

想象一个经典场景:生产者-消费者问题。

  • 生产者不断生产数据,放入共享队列。
  • 消费者不断从队列取出数据消费。
  • 如果队列空,消费者必须等待;如果队列满,生产者必须等待。

用简单的锁(Lock)可以保证队列操作线程安全,但无法实现“等待特定条件成立”的功能。
条件变量(Condition)就是为了解决这类“等待/通知”场景而设计的同步原语。


2. 条件变量的基本组成

Python的threading.Condition包含三个核心部分:

  1. 一个底层锁(可以是RLock或用户提供的锁)。
  2. 一个等待池wait时线程进入等待状态)。
  3. 通知机制notify/notify_all唤醒等待线程)。

创建方式

import threading
cond = threading.Condition()  # 内部自动创建RLock
# 或使用已有锁
lock = threading.Lock()
cond = threading.Condition(lock)

3. 条件变量的核心方法

3.1 acquire()release()

条件变量内部锁的获取/释放,与普通锁用法一致:

cond.acquire()  # 获取内部锁
try:
    # 操作共享资源
    if not condition_met:
        cond.wait()  # 释放锁并等待
finally:
    cond.release()  # 释放锁

通常使用with语句简化:

with cond:
    if not condition_met:
        cond.wait()

3.2 wait(timeout=None)

  • 作用:释放内部锁,将当前线程放入等待池,线程进入阻塞状态。
  • 被唤醒后:重新获取锁(可能需与其他线程竞争),然后继续执行。
  • 重要:调用wait()前必须先获取锁,否则报错RuntimeError

为什么先释放锁?
如果持有锁等待,其他线程无法修改条件,导致死锁。


3.3 notify(n=1)notify_all()

  • notify():唤醒等待池中最多n个线程(FIFO顺序)。
  • notify_all():唤醒所有等待线程。
  • 注意:调用这些方法前也必须先获取锁。

唤醒后发生了什么?
被唤醒的线程会从wait()返回,但需要重新竞争获取内部锁,获取成功后才能真正继续执行。


4. 生产者-消费者完整示例

import threading
import time
import random

queue = []
MAX_ITEMS = 5
condition = threading.Condition()

class Producer(threading.Thread):
    def run(self):
        global queue
        while True:
            with condition:  # 获取锁
                if len(queue) == MAX_ITEMS:
                    print("队列已满,生产者等待")
                    condition.wait()  # 释放锁,等待
                item = random.randint(1, 100)
                queue.append(item)
                print(f"生产: {item}, 队列长度: {len(queue)}")
                condition.notify()  # 唤醒一个消费者
            time.sleep(random.random())

class Consumer(threading.Thread):
    def run(self):
        global queue
        while True:
            with condition:
                if not queue:
                    print("队列为空,消费者等待")
                    condition.wait()  # 释放锁,等待
                item = queue.pop(0)
                print(f"消费: {item}, 队列长度: {len(queue)}")
                condition.notify()  # 唤醒一个生产者
            time.sleep(random.random() * 2)

# 启动线程
Producer().start()
Consumer().start()

执行流程

  1. 消费者先运行,发现队列空,调用wait()释放锁并等待。
  2. 生产者获取锁,生产数据,调用notify()唤醒消费者。
  3. 消费者被唤醒,但需等待生产者退出with块释放锁后,才能获取锁并从wait()返回。

5. 条件变量的常见陷阱

5.1 虚假唤醒(Spurious Wakeup)

操作系统可能无故唤醒等待线程,因此wait返回后必须重新检查条件:

with cond:
    while not condition_met:  # 用while而不是if
        cond.wait()
    # 条件满足,继续执行

5.2 通知丢失

如果先notify()wait(),通知可能丢失。确保状态改变和通知在同一个锁内:

with cond:
    condition_met = True
    cond.notify()  # 在释放锁前通知

5.3 死锁风险

嵌套使用多个条件变量时,需小心锁的获取顺序,避免循环等待。


6. 条件变量与threading.Event的区别

  • Event:一次性广播,适合简单开关场景(如线程启动信号)。
  • Condition:可多次等待/通知,与共享状态变更紧密耦合。

7. 实际应用场景

  1. 线程池任务调度:工作线程等待任务队列非空。
  2. 资源池管理:数据库连接池中,线程等待可用连接。
  3. 并行计算屏障:多个线程同步到某个阶段后继续。

8. 总结要点

  • 条件变量 = 锁 + 等待池 + 通知机制。
  • 必须用锁保护共享状态的修改和检查。
  • 永远在while循环中调用wait(),防范虚假唤醒。
  • notify()wait()必须在已获取锁的情况下调用。

通过条件变量,我们可以实现高效的线程间协作,避免忙等待(busy-waiting),提升并发程序性能。

Python中的并发编程:线程同步与条件变量(Condition)详解 1. 问题引入:为什么要用条件变量? 想象一个经典场景:生产者-消费者问题。 生产者不断生产数据,放入共享队列。 消费者不断从队列取出数据消费。 如果队列空,消费者必须等待;如果队列满,生产者必须等待。 用简单的锁( Lock )可以保证队列操作线程安全,但无法实现“等待特定条件成立”的功能。 条件变量( Condition )就是为了解决这类“等待/通知”场景而设计的同步原语。 2. 条件变量的基本组成 Python的 threading.Condition 包含三个核心部分: 一个底层锁 (可以是 RLock 或用户提供的锁)。 一个等待池 ( wait 时线程进入等待状态)。 通知机制 ( notify / notify_all 唤醒等待线程)。 创建方式 : 3. 条件变量的核心方法 3.1 acquire() 和 release() 条件变量内部锁的获取/释放,与普通锁用法一致: 通常使用 with 语句简化: 3.2 wait(timeout=None) 作用 :释放内部锁,将当前线程放入等待池,线程进入阻塞状态。 被唤醒后 :重新获取锁(可能需与其他线程竞争),然后继续执行。 重要 :调用 wait() 前必须先获取锁,否则报错 RuntimeError 。 为什么先释放锁? 如果持有锁等待,其他线程无法修改条件,导致死锁。 3.3 notify(n=1) 和 notify_all() notify() :唤醒等待池中最多 n 个线程(FIFO顺序)。 notify_all() :唤醒所有等待线程。 注意 :调用这些方法前也必须先获取锁。 唤醒后发生了什么? 被唤醒的线程会从 wait() 返回,但需要重新竞争获取内部锁,获取成功后才能真正继续执行。 4. 生产者-消费者完整示例 执行流程 : 消费者先运行,发现队列空,调用 wait() 释放锁并等待。 生产者获取锁,生产数据,调用 notify() 唤醒消费者。 消费者被唤醒,但需等待生产者退出 with 块释放锁后,才能获取锁并从 wait() 返回。 5. 条件变量的常见陷阱 5.1 虚假唤醒(Spurious Wakeup) 操作系统可能无故唤醒等待线程,因此 wait 返回后必须重新检查条件: 5.2 通知丢失 如果先 notify() 后 wait() ,通知可能丢失。确保状态改变和通知在同一个锁内: 5.3 死锁风险 嵌套使用多个条件变量时,需小心锁的获取顺序,避免循环等待。 6. 条件变量与 threading.Event 的区别 Event :一次性广播,适合简单开关场景(如线程启动信号)。 Condition :可多次等待/通知,与共享状态变更紧密耦合。 7. 实际应用场景 线程池任务调度 :工作线程等待任务队列非空。 资源池管理 :数据库连接池中,线程等待可用连接。 并行计算屏障 :多个线程同步到某个阶段后继续。 8. 总结要点 条件变量 = 锁 + 等待池 + 通知机制。 必须用锁保护共享状态的修改和检查。 永远在 while 循环中调用 wait() ,防范虚假唤醒。 notify() 和 wait() 必须在已获取锁的情况下调用。 通过条件变量,我们可以实现高效的线程间协作,避免忙等待(busy-waiting),提升并发程序性能。