线程同步之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 的工作流程,我画出来帮你加深理解:

RLock 可重入锁工作流程 线程 A 线程 B RLock 对象 持有线程: 线程 A 计数器: 2 acquire() 第1次 计数器: 0→1 acquire() 第2次 计数器: 1→2 ✅ acquire() 尝试 ❌ 被阻塞 release() 第2次 计数器: 2→1 release() 第1次 计数器: 1→0 只有计数器归零时,其他线程才能获取锁

从这张图可以看得很清楚:线程 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,能省不少事。


可可精品资料库