Python 多线程同步是确保多个线程安全、有序地访问共享资源,避免数据竞争(Race Condition)和保证数据一致性的关键技术。Python 的 threading 模块提供了多种同步原语,每种都有其特定的适用场景。以下将详细介绍这些核心机制、使用方法和最佳实践。
一、线程锁(Lock)
线程锁是最基础、最常用的同步工具,用于确保在任意时刻只有一个线程能进入被保护的代码段(临界区)。
基本用法 : 通过 threading.Lock() 创建锁对象,使用 acquire() 获取锁,release() 释放锁。推荐使用 with 语句自动管理锁的获取和释放,避免因异常导致锁无法释放。
import threading
# 创建锁
lock = threading.Lock()
shared_data = 0
def increment():
global shared_data
for _ in range(100000):
# 获取锁
lock.acquire()
try:
shared_data += 1
finally:
# 释放锁
lock.release()
# 使用 with 语句更安全
def increment_safe():
global shared_data
for _ in range(100000):
with lock: # 自动获取和释放锁
shared_data += 1
# 创建线程
t1 = threading.Thread(target=increment_safe)
t2 = threading.Thread(target=increment_safe)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"最终结果: {shared_data}") # 应该是 200000
可重入锁(RLock) : 普通锁(Lock)在同一线程内不可重入,连续调用 acquire() 会导致死锁。threading.RLock()(可重入锁)允许同一线程多次获取锁,通常用于递归函数或多次调用需要同步的同一方法。
import threading
rlock = threading.RLock()
def recursive_func(n):
with rlock:
print(f"Entering Level {n}")
if n > 0:
recursive_func(n - 1)
print(f"Exiting Level {n}")
print("Starting thread with recursive RLock example")
thread = threading.Thread(target=recursive_func, args=(3,))
thread.start()
thread.join()
print("Thread completed successfully")
二、信号量(Semaphore)
信号量用于控制同时访问某个共享资源的线程数量,其内部维护一个计数器。threading.Semaphore(value=N) 初始化时指定最大并发数,acquire() 减少计数,release() 增加计数。
import threading
import time
# 最多允许3个线程同时访问
sem = threading.Semaphore(3)
def access_resource(thread_id):
print(f"Thread {thread_id} is waiting to access")
with sem:
print(f"Thread {thread_id} has acquired the semaphore and is working")
time.sleep(1)
print(f"Thread {thread_id} is releasing the semaphore")
print("Starting threads with semaphore limit of 3...")
threads = []
for i in range(10):
t = threading.Thread(target=access_resource, args=(i,))
threads.append(t)
t.start()
time.sleep(0.1) # 稍微延迟创建线程,便于观察
for t in threads:
t.join()
print("All threads completed")
threading.BoundedSemaphore 是信号量的变体,可防止计数超过初始值。
python
import threading
import time
# 创建一个有界信号量,最多允许3个线程同时访问
bounded_sem = threading.BoundedSemaphore(3)
def normal_access(thread_id):
"""正常使用有界信号量的函数"""
print(f"Thread {thread_id} is waiting to access")
with bounded_sem:
print(f"Thread {thread_id} has acquired the bounded semaphore and is working")
time.sleep(1)
print(f"Thread {thread_id} is releasing the bounded semaphore")
def demonstrate_bounded_semaphore_error():
"""演示有界信号量防止过度释放的特性"""
print("\n演示BoundedSemaphore防止过度释放的特性:")
# 先获取信号量
print("尝试获取信号量...")
bounded_sem.acquire()
print("信号量获取成功")
# 正常释放一次
print("正常释放一次信号量...")
bounded_sem.release()
print("信号量正常释放")
# 尝试过度释放
try:
print("尝试过度释放信号量(这将引发ValueError异常)...")
bounded_sem.release() # 这里会抛出ValueError
print("如果看到这条消息,说明有界信号量没有正常工作!")
except ValueError as e:
print(f"成功捕获异常: {e}")
print("这证明BoundedSemaphore可以防止过度释放的情况发生")
print("=== 有界信号量(BoundedSemaphore)示例程序 ===")
print("1. 首先运行多线程正常访问的场景")
# 创建并启动多个线程
threads = []
for i in range(5):
t = threading.Thread(target=normal_access, args=(i,))
threads.append(t)
t.start()
time.sleep(0.2) # 稍微延迟创建线程,便于观察
# 等待所有线程完成
for t in threads:
t.join()
print("\n所有线程完成正常访问")
# 演示有界信号量的错误检测功能
demonstrate_bounded_semaphore_error()
print("\n=== 程序结束 ===")
三、条件变量(Condition)
条件变量用于复杂的线程间协调,允许一个或多个线程等待特定条件成立后再继续执行。它通常与一个锁关联,提供了 wait()、notify() 和 notify_all() 方法。
典型应用:生产者-消费者模型
python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
生产者-消费者模型实现
本程序演示使用threading.Condition(条件变量)实现经典的生产者-消费者模型。
生产者线程生成随机数并放入固定大小的缓冲区,消费者线程从缓冲区取出数据。
当缓冲区满时,生产者等待;当缓冲区空时,消费者等待。
"""
import threading
import time
import random
# 共享缓冲区,用于存储生产者生产的物品
buffer = []
# 缓冲区最大容量
MAX_SIZE = 10
# 创建条件变量,用于线程间通信和同步
# Condition内部维护一个锁,提供wait()、notify()等方法进行线程协作
condition = threading.Condition()
class Producer(threading.Thread):
"""
生产者类,继承自threading.Thread
负责生成随机数并添加到缓冲区
"""
def run(self):
"""
线程运行方法
使用条件变量确保线程安全的缓冲区访问
"""
global buffer
# 无限循环生产物品
while True:
# 获取条件变量的锁
with condition:
# 检查缓冲区是否已满
# 注意:此处应使用while循环而非if语句,防止虚假唤醒
while len(buffer) == MAX_SIZE:
print("Buffer full, producer waiting")
# 释放锁并等待,直到被其他线程通知
condition.wait()
# 生产一个随机物品
item = random.randint(1, 100)
# 将物品添加到缓冲区
buffer.append(item)
print(f"Produced {item}, buffer: {buffer}")
# 通知一个等待的消费者线程
condition.notify()
# 生产间隔随机时间,模拟生产过程
time.sleep(random.random())
class Consumer(threading.Thread):
"""
消费者类,继承自threading.Thread
负责从缓冲区取出物品进行消费
"""
def run(self):
"""
线程运行方法
使用条件变量确保线程安全的缓冲区访问
"""
global buffer
# 无限循环消费物品
while True:
# 获取条件变量的锁
with condition:
# 检查缓冲区是否为空
# 同样使用while循环防止虚假唤醒
while not buffer:
print("Buffer empty, consumer waiting")
# 释放锁并等待,直到被其他线程通知
condition.wait()
# 从缓冲区取出第一个物品(FIFO原则)
item = buffer.pop(0)
print(f"Consumed {item}, buffer: {buffer}")
# 通知一个等待的生产者线程
condition.notify()
# 消费间隔随机时间,模拟消费过程
time.sleep(random.random())
# 程序入口点
if __name__ == "__main__":
print("生产者-消费者模型演示程序启动...")
print(f"缓冲区最大容量: {MAX_SIZE}")
# 创建并启动生产者线程
Producer().start()
# 创建并启动消费者线程
Consumer().start()
print("线程已启动,按Ctrl+C停止程序")
wait_for(predicate, timeout) 方法可等待某个条件表达式为真,使代码更简洁。
四、事件(Event)
事件是一种简单的线程间通信机制,一个线程发出"事件已发生"的信号,其他线程等待该信号。通过 threading.Event() 创建事件对象,主要方法有 set()、clear() 和 wait()。
python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Threading.Event示例程序
本程序演示了Python中threading.Event的基本用法。
Event对象是线程间同步的一种机制,类似于一个信号标志,
可以控制线程的执行和等待状态。
"""
import threading
import time
# 创建Event对象,初始状态为False
event = threading.Event()
def waiter():
"""
等待者线程函数
该函数模拟一个需要等待某个事件发生才能继续执行的线程
当event被设置(set)时,wait()方法才会解除阻塞
"""
print("Waiter: waiting for event")
# 阻塞当前线程,直到event被设置
event.wait() # 阻塞直到事件被设置
print("Waiter: event received, proceeding")
def setter():
"""
设置者线程函数
该函数模拟一个在某个时刻触发事件的线程
通过调用event.set()方法,将事件标志设置为True,
唤醒所有等待该事件的线程
"""
# 模拟耗时操作
time.sleep(2)
print("Setter: setting event")
# 设置事件,唤醒所有等待的线程
event.set() # 设置事件,唤醒所有等待的线程
# 创建并启动等待者线程
threading.Thread(target=waiter).start()
# 创建并启动设置者线程
threading.Thread(target=setter).start()
五、其他同步工具与线程安全数据结构
- 屏障(Barrier):使一组线程必须全部到达某个点后才能继续执行,适用于分阶段任务。
- 线程本地数据(threading.local):为每个线程创建独立的变量副本,避免共享,从根本上解决同步问题。
- 队列(Queue) :
queue模块提供的Queue、LifoQueue、PriorityQueue是线程安全的,内部已实现锁机制,非常适合生产者-消费者模式,无需显式同步。
六、全局解释器锁(GIL)的影响与最佳实践
GIL 的影响 : Python 的全局解释器锁(GIL)确保同一时刻只有一个线程执行 Python 字节码。这意味着对于 CPU 密集型任务 ,多线程无法利用多核优势,性能提升有限甚至可能下降。但对于 I/O 密集型任务(如网络请求、文件读写),线程在等待 I/O 时会释放 GIL,因此多线程仍能有效提升并发性能。
多线程同步的最佳实践:
- 选择合适的工具 :简单互斥用
Lock,控制并发数量用Semaphore,复杂协调用Condition或Event,数据传递用Queue。 - 避免死锁 :确保多个锁的获取顺序一致;考虑使用带超时的
acquire(timeout);优先使用可重入锁(RLock)或高层抽象(如Queue)。 - 最小化锁的持有时间:只将必须同步的代码放入临界区,尽快释放锁以提高并发性。
- 考虑使用线程安全的数据结构 :如
queue.Queue,可减少手动同步的复杂度。 - 对于CPU密集型任务考虑多进程 :使用
multiprocessing模块绕过 GIL 限制,充分利用多核CPU。
总结
Python 提供了丰富的线程同步机制,从基础的 Lock、Semaphore 到更高级的 Condition、Event 和 Barrier,以及线程安全的 Queue。选择何种机制取决于具体场景:保护简单共享变量可用 Lock;控制资源并发数用 Semaphore;实现线程间依赖和通知用 Condition 或 Event;在线程间传递数据则优先使用 Queue。同时,必须注意 GIL 对多线程并行的限制,并遵循避免死锁、最小化锁范围等最佳实践,才能编写出高效、健壮的多线程程序。