在Python多线程编程中,由于全局解释器锁GIL的存在,同一时刻只有一个线程能执行Python字节码,但当线程执行IO操作时会释放GIL,多个线程仍然可能同时操作共享资源,导致数据错乱,这时候就需要线程同步原语来协调线程执行顺序,保证并发安全。

线程同步原语基本概念
线程同步原语是操作系统或者编程语言提供的用于协调多个线程执行、避免资源竞争的工具,核心作用是保证同一时刻只有一个或者一组线程能访问共享资源,常见的同步原语包括锁、信号量、事件、条件变量等。
在Python的threading模块中,提供了多种线程同步原语,其中锁是最基础的同步工具,普通锁Lock同一时刻只能被一个线程获取,获取锁的线程释放锁之后,其他线程才能尝试获取。
RLock可重入锁的特性
RLock即可重入锁,是Lock的扩展版本,它的核心特性是同一个线程可以多次获取该锁而不会被阻塞,普通Lock如果同一个线程连续两次获取,就会出现死锁。
RLock内部维护了两个状态:锁的持有者线程和重入次数,当线程第一次获取RLock时,重入次数设为1,后续同一个线程再次获取时,重入次数加1,线程释放锁时重入次数减1,只有当重入次数减到0时,锁才会真正被释放,其他线程才能获取。
RLock与普通Lock的对比
| 对比项 | Lock | RLock |
|---|---|---|
| 同一线程多次获取 | 会死锁 | 不会死锁,重入次数累加 |
| 适用场景 | 简单的单次加锁场景 | 递归调用、嵌套加锁场景 |
| 实现复杂度 | 简单 | 相对复杂,维护持有者和重入次数 |
RLock并发安全实践示例
下面通过一个共享变量累加的场景,展示使用RLock保证并发安全的方法,首先是不使用任何锁的情况,会出现结果错误:
import threading
# 共享变量
count = 0
def add_count():
global count
for _ in range(100000):
count += 1
# 创建两个线程
t1 = threading.Thread(target=add_count)
t2 = threading.Thread(target=add_count)
t1.start()
t2.start()
t1.join()
t2.join()
# 预期结果是200000,实际结果会小于该值
print(f"无锁时最终count值: {count}")
接下来使用RLock对共享资源的操作进行加锁,保证同一时刻只有一个线程能修改count:
import threading
# 共享变量
count = 0
# 创建RLock对象
rlock = threading.RLock()
def add_count():
global count
# 获取RLock
rlock.acquire()
try:
for _ in range(100000):
count += 1
finally:
# 释放RLock
rlock.release()
# 创建两个线程
t1 = threading.Thread(target=add_count)
t2 = threading.Thread(target=add_count)
t1.start()
t2.start()
t1.join()
t2.join()
# 最终结果为200000,符合预期
print(f"使用RLock时最终count值: {count}")
再展示RLock的可重入特性,同一个线程多次获取锁不会死锁:
import threading
rlock = threading.RLock()
def func_a():
# 第一次获取锁
rlock.acquire()
print("func_a获取锁成功")
func_b()
rlock.release()
def func_b():
# 同一个线程第二次获取锁,不会阻塞
rlock.acquire()
print("func_b获取锁成功")
rlock.release()
# 创建线程执行func_a
t = threading.Thread(target=func_a)
t.start()
t.join()
使用RLock的注意事项
- RLock的获取和释放必须成对出现,获取多少次就要释放多少次,否则锁无法被其他线程获取,会导致死锁。
- 如果不需要可重入特性,优先使用普通Lock,因为RLock的实现更复杂,性能开销比Lock稍大。
- 尽量缩小锁的覆盖范围,只把操作共享资源的代码放在加锁和释放锁之间,减少锁的持有时间,提升多线程执行效率。
线程同步的核心目标是保证共享资源的一致性,选择同步原语时要根据实际场景判断,RLock适合需要同一个线程多次获取锁的场景,合理使用才能既保证并发安全又不影响程序性能。