线程同步之RLock:可重入锁
说到多线程编程里的锁,大家最先想到的肯定是 Lock。但我在实际项目中遇到过一个问题:同一个线程,能不能多次获取同一把锁?
你可能会说:「这有什么好问的?锁就是用来互斥的,自己锁自己,那不是死锁了吗?」
嗯,正常情况下确实是这样。但有一种特殊的锁,它允许同一个线程多次获取——这就是我们今天要聊的 RLock(可重入锁)。
什么是可重入锁?
说白了,RLock 就是一把「认识主人」的锁。它会记录两样东西:
- 当前持有锁的线程是谁
- 这个线程获取了多少次
当同一个线程再次请求这把锁时,RLock 不会阻塞,而是直接放行。每次获取,内部的计数器就加 1;每次释放,计数器就减 1。只有计数器归零了,其他线程才能拿到这把锁。
核心区别一句话总结:
Lock 是「认锁不认人」,RLock 是「既认锁又认人」。
Lock 与 RLock 的区别
我习惯用一张表来对比,这样最直观:
| 对比项 | Lock(普通锁) | RLock(可重入锁) |
|---|---|---|
| 同一线程多次获取 | ❌ 会死锁 | ✅ 允许 |
| 内部实现 | 简单的二进制状态 | 计数器 + 线程标识 |
| 释放次数 | 一次 release 即可 | 需与 acquire 次数匹配 |
| 适用场景 | 简单的互斥访问 | 递归函数、嵌套锁 |
为什么会这样?我们来看个例子。
递归场景下的使用
我记得有一次写一个递归函数,需要保证某个共享资源不被其他线程干扰。当时我用了 Lock,结果程序一跑就卡死了。
代码大概是这样的:
import threading
lock = threading.Lock()
shared_data = []
def recursive_func(n):
if n <= 0:
return
with lock:
shared_data.append(n)
# 这里递归调用自己,又会尝试获取锁
recursive_func(n - 1)
# 运行一下试试?
recursive_func(5)
print(shared_data)
你猜怎么着?程序直接死锁了。因为第一次进入 with lock 时,当前线程已经持有了锁。当递归调用自己时,再次请求同一把锁——Lock 可不管你是不是同一个线程,它只会说:「锁被占着呢,等着吧。」于是自己等自己,死锁了。
我曾经踩过的坑:
用 Lock 写递归函数,结果程序卡死。排查了半天才发现是锁的问题。后来换成 RLock,问题迎刃而解。
换成 RLock 试试:
import threading
rlock = threading.RLock()
shared_data = []
def recursive_func(n):
if n <= 0:
return
with rlock:
shared_data.append(n)
# 同一个线程再次获取,没问题!
recursive_func(n - 1)
recursive_func(5)
print(shared_data) # 输出: [5, 4, 3, 2, 1]
完美运行。RLock 知道是同一个线程在请求,直接放行,计数器加 1。等递归返回时,一层层释放,计数器慢慢减到 0。
RLock 的计数器机制
这里有个细节要注意:acquire 和 release 必须成对出现。
我建议你在写代码时,用 with 语句来管理 RLock,这样最安全。如果你手动调用 acquire 和 release,一定要保证数量匹配。
rlock = threading.RLock()
# 正确用法
rlock.acquire() # 计数器: 1
rlock.acquire() # 计数器: 2
# ... 做一些操作 ...
rlock.release() # 计数器: 1
rlock.release() # 计数器: 0
# 错误用法:多 release 一次
rlock.acquire() # 计数器: 1
rlock.release() # 计数器: 0
rlock.release() # ❌ RuntimeError: 释放未持有的锁
小技巧:
我个人习惯在递归函数里用 with rlock:,而不是手动 acquire/release。这样既清晰又安全,不用担心忘记释放。
什么时候该用 RLock?
其实大部分场景下,普通的 Lock 就够用了。但如果你遇到下面这些情况,RLock 会是更好的选择:
- 递归函数中需要加锁——就像上面的例子
- 一个函数调用另一个加锁的函数——比如 A 函数获取了锁,然后调用 B 函数,B 函数也需要同一把锁
- 面向对象中的加锁方法——父类方法加了锁,子类重写时又调用了父类方法
举个例子:
import threading
class Counter:
def __init__(self):
self.value = 0
self.lock = threading.RLock()
def increment(self):
with self.lock:
self.value += 1
def increment_twice(self):
with self.lock:
self.increment() # 再次获取同一把锁
self.increment()
counter = Counter()
counter.increment_twice()
print(counter.value) # 输出: 2
你看,increment_twice 获取了锁,然后调用了 increment,后者又尝试获取同一把锁。如果是普通 Lock,这里就死锁了。但 RLock 知道是同一个线程,直接放行。
用 SVG 画一张图,理清思路
下面这张图展示了 RLock 的工作流程,我画出来帮你加深理解:
从这张图可以看得很清楚:线程 A 连续两次获取 RLock,计数器从 0 涨到 2。线程 B 想插队?没门,老老实实等着。等线程 A 释放两次,计数器归零,线程 B 才能拿到锁。
避坑指南
最后分享几个我踩过的坑:
- 不要滥用 RLock——能用 Lock 的地方就别用 RLock,毕竟 RLock 有额外的计数开销
- 注意 release 次数——多 release 一次会抛异常,少 release 一次会导致其他线程永远拿不到锁
- RLock 不能跨线程——一个线程获取了 RLock,另一个线程不能释放它,会抛 RuntimeError
我曾经犯过的错:
有一次在 try-finally 里手动管理 RLock,finally 块里多写了一个 release。结果程序跑着跑着就崩了,报错说「释放了未持有的锁」。后来我改成 with 语句,再也没出过这种问题。
嗯,RLock 的内容就这些。说白了,它就是一把「认识主人」的锁,专门解决同一个线程重复获取锁的问题。下次写递归函数或者嵌套调用时,记得用 RLock 代替 Lock,能省不少事。
可可精品资料库