RLock适用于同一线程需多次获取同一锁的递归或重入场景,允许重复acquire并需对应次数release;Semaphore则用于控制并发数,允许多线程共享资源但限制总量,acquire与release可跨线程。

RLock 适合递归加锁的线程安全场景
当一个线程需要多次获取同一把锁(比如在递归调用、重入方法中),用普通 Lock 会直接阻塞自己,导致死锁。而 RLock(可重入锁)允许同一线程重复调用 acquire(),只要对应次数的 release() 就能真正释放锁。
常见错误现象:threading.Lock 在递归函数里卡住,CPU 占用低但程序不继续;报错信息里没有异常,只是静默挂起。
使用场景:
- 类方法中调用自身其他加锁方法(如
add()内部调用update(),两者都需保护共享状态) - 实现线程安全的缓存装饰器,内部可能多次访问被锁保护的字典
- 构造复杂对象时,初始化过程嵌套调用多个需同步的子步骤
注意点:
立即学习“Python免费学习笔记(深入)”;
-
RLock比Lock开销略大,因为要维护持有线程 ID 和计数器 - 必须由同一个线程完成所有
acquire()和release(),跨线程调用release()会抛RuntimeError: release unlocked lock - 不解决“锁顺序不一致”导致的死锁,只解决“自己锁自己”的问题
Semaphore 更适合资源池或并发数控制
Semaphore 的核心不是“互斥”,而是“限制同时访问的线程数量”。它内部维护一个计数器,acquire() 减一,release() 加一,计数器为 0 时阻塞。
典型使用场景:
- 控制对数据库连接池、HTTP 客户端、GPU 显存等有限资源的并发访问
- 限流:比如最多允许 5 个线程同时执行耗时任务,其余排队
- 模拟生产者-消费者中“槽位”数量(比
Queue更轻量,但不带数据传递)
和 Lock/RLock 的关键差异:
-
acquire()可由 A 线程调用,release()可由 B 线程调用(即不要求同线程) - 初始化时传入的数值就是最大并发数,设为 1 时行为接近
Lock,但语义不同、开销略高 - 不保证临界区的排他性——多个线程可以同时在临界区内,只要没超限额
容易踩的坑:
系统简介逍遥内容管理系统(CarefreeCMS)是一款功能强大、易于使用的内容管理平台,采用前后端分离架构,支持静态页面生成,适用于个人博客、企业网站、新闻媒体等各类内容发布场景。核心特性1、模板套装系统 - 支持多套模板自由切换,快速定制网站风格2、静态页面生成 - 一键生成纯静态HTML页面,访问速度快,SEO友好3、文章管理 - 支持富文本编辑、草稿保存、文章属性标记、自动提取SEO4、全
- 忘记调用
release(),导致计数器永远卡在 0,后续所有线程永久阻塞 - 在异常路径中未做
try/finally保护,造成资源泄漏(建议用上下文管理器) - 误以为
Semaphore能替代条件变量,其实它不提供“等待某个状态成立”的能力
别用 RLock 去代替 Semaphore 控制并发数
有人看到 RLock 也能“控制进入”,就试图靠它限制并发线程数,这是错的。因为 RLock 的重入特性意味着:只要一个线程拿到锁,它就能无限次 acquire(),完全绕过限制。
示例问题代码:
lock = threading.RLock() # 错误:下面这段对并发数毫无约束力 lock.acquire() do_work() lock.release()
这和用 Lock 效果一样,且更易引发隐藏 bug。真正要控并发,请明确用 Semaphore(value=3)。
另一个混淆点:BoundedSemaphore 是 Semaphore 的子类,会在 release() 超出初始值时抛异常,适合调试阶段捕获误释放。
实际选型看“谁该放行”和“放行依据”
决定用哪个,关键是回答两个问题:
- 放行依据是“是不是同一个线程”?→ 选
RLock - 放行依据是“当前已占用资源数是否达到上限”?→ 选
Semaphore - 需要严格一对一加锁/解锁,且不允许跨线程释放?→ 只能用
Lock或RLock
性能上差异不大,但语义错位会导致极难复现的竞态。比如在连接池里误用 RLock,可能让单个线程占满所有连接却不释放,而其他线程干等。
最常被忽略的一点:Python 的 GIL 让纯计算密集型任务无法真正并行,所以这些同步原语主要保护的是 I/O、共享数据结构(如 dict、list)、或 C 扩展中释放 GIL 后的临界区——别在无共享的 CPU 绑定循环里白加锁。









