Python中的线程安全与同步原语(Lock、RLock、Semaphore、Condition、Event)
字数 1536 2025-11-16 23:55:00
Python中的线程安全与同步原语(Lock、RLock、Semaphore、Condition、Event)
1. 问题背景
在Python多线程编程中,多个线程可能同时访问共享资源(如变量、文件、数据库连接等),导致数据不一致或逻辑错误。例如:
import threading
counter = 0
def increment():
global counter
for _ in range(100000):
counter += 1
threads = []
for _ in range(10):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
for t in threads:
t.join()
print(counter) # 结果可能小于1000000
问题分析:
counter += 1 并非原子操作,实际执行时会被拆解为多个步骤(读取值、计算新值、写入结果),线程切换可能导致数据覆盖。
2. 解决方案:同步原语
Python的threading模块提供了多种同步工具,确保线程间协调访问共享资源。
2.1 Lock(互斥锁)
作用:保证同一时刻只有一个线程能执行关键代码段。
使用步骤:
- 创建锁:
lock = threading.Lock() - 获取锁:
lock.acquire() - 释放锁:
lock.release() - 推荐使用上下文管理器(自动释放锁):
def increment():
global counter
for _ in range(100000):
with lock: # 自动获取和释放锁
counter += 1
lock = threading.Lock() # 修复代码:锁需在函数外定义
注意:锁必须被所有线程共享,且需避免重复释放锁(会导致RuntimeError)。
2.2 RLock(可重入锁)
问题场景:如果一个线程在持有锁的情况下再次尝试获取同一把锁,普通Lock会阻塞自身(死锁)。
RLock特性:允许同一线程多次获取锁,需释放相同次数才能彻底解锁。
rlock = threading.RLock()
def nested_lock():
with rlock: # 第一次获取锁
with rlock: # 同一线程可再次获取
print("双重锁定安全")
threading.Thread(target=nested_lock).start()
2.3 Semaphore(信号量)
作用:控制同时访问资源的线程数量(如连接池限流)。
示例:限制最多3个线程同时运行:
semaphore = threading.Semaphore(3)
def task():
with semaphore:
print(f"{threading.current_thread().name} 执行任务")
# 模拟耗时操作
time.sleep(1)
for i in range(10):
threading.Thread(target=task).start()
2.4 Condition(条件变量)
作用:让线程等待特定条件满足后再执行,常用于生产者-消费者模型。
核心方法:
wait():释放锁并等待通知notify(n):唤醒至少n个等待线程notify_all():唤醒所有等待线程
queue = []
condition = threading.Condition()
def producer():
with condition:
queue.append("数据")
condition.notify() # 通知一个消费者
def consumer():
with condition:
while not queue:
condition.wait() # 队列空时等待
item = queue.pop()
print(f"消费: {item}")
2.5 Event(事件)
作用:线程间简单通信机制,一个线程发出信号,其他线程等待信号。
核心方法:
set():设置事件为Trueclear():重置事件为Falsewait():阻塞直到事件为True
event = threading.Event()
def waiter():
print("等待事件触发...")
event.wait() # 阻塞直到set()被调用
print("事件已触发!")
def setter():
time.sleep(2)
event.set() # 唤醒所有等待线程
threading.Thread(target=waiter).start()
threading.Thread(target=setter).start()
3. 对比与选型指南
| 同步原语 | 适用场景 | 特点 |
|---|---|---|
| Lock | 互斥访问简单共享资源 | 轻量级,不支持重入 |
| RLock | 嵌套锁需求(如递归函数) | 同一线程可多次获取 |
| Semaphore | 限制并发线程数 | 计数器控制 |
| Condition | 复杂条件等待(如生产者-消费者) | 需与锁结合使用,支持等待/通知机制 |
| Event | 简单信号通知(如启动/停止信号) | 布尔标志,一次性通信 |
4. 实战注意事项
- 避免死锁:按固定顺序获取锁,或使用超时机制(
lock.acquire(timeout=5))。 - 性能影响:同步原语会引入开销,尽量缩小锁的覆盖范围(如减少锁内代码)。
- GIL限制:在CPU密集型任务中,多线程可能无法充分利用多核,建议改用多进程(
multiprocessing模块)。
通过合理选择同步工具,可确保多线程程序既安全又高效。