死锁是静默卡住而非报错,表现为线程停在acquire()、CPU占用低;可通过Ctrl+C或kill -USR1查看堆栈确认;加timeout超时、统一锁获取顺序、命名锁及慎用RLock是关键应对措施。

死锁不是报错,是静默卡住
Python 多线程死锁不会抛出异常,程序会突然“不动了”,CPU 占用极低,但所有线程都停在 Lock.acquire() 或 RLock.acquire() 上。最直接确认方式:Ctrl+C 触发 KeyboardInterrupt,看堆栈是否大量停在 acquire 调用;Linux/macOS 下可用 kill -USR1 <pid> 打印线程栈,Windows 则需调用 sys._current_frames() 主动抓取。
acquire(timeout=) 是最可控的防御手段
给锁加超时,是干预死锁最轻量、最明确的方式。它不依赖代码重构,能快速暴露卡点并让线程主动退出。
-
with lock:语法无法传 timeout,必须显式调用lock.acquire(timeout=2) - 务必检查返回值:
if not lock.acquire(timeout=2): raise RuntimeError("lock timeout"),漏掉这步等于没加 - 超时值要结合业务:数据库操作建议
timeout=3,缓存更新可设timeout=0.5,不能盲目设为 0(非阻塞) - 成功 acquire 后,必须配对
release(),推荐用try/finally包裹,防止中间异常导致锁未释放
统一锁获取顺序才是根治循环等待的关键
两个线程分别按不同顺序获取 lock_a 和 lock_b,就构成经典循环等待。靠日志或超时只能“救火”,真正防患于未然得靠设计约束。
- 给锁排序:用
sorted([lock_a, lock_b], key=id)按对象 ID 升序,再依次acquire() - 封装安全函数:写一个
acquire_all(*locks),内部自动排序 + 逐个 acquire,避免手误 - 禁止反向释放:释放顺序无需严格逆序,但绝不能在持有小 ID 锁时去申请更大 ID 锁——这会破坏顺序性
- 每个锁加
name参数(如threading.Lock(name="user_db")),否则日志里全是<unlocked _thread.lock object at 0x...>,无法定位
RLock 不防死锁,反而容易埋雷
threading.RLock 只解决同一线程重复加锁崩溃的问题,对多线程交叉等待完全无效。它甚至因“不报错”掩盖设计缺陷,比普通 Lock 更危险。
立即学习“Python免费学习笔记(深入)”;
- 误把
RLock当“万能锁”:多个函数共用同一把rlock,但各自又尝试获取其他锁,极易触发循环等待 -
RLock的 acquire 计数器不对外暴露,日志无法判断当前持有次数,排查困难 - 若函数 A 拿了
rlock后调用函数 B,B 再去拿另一把lock_a,而另一线程正持lock_a等这把rlock——死锁立刻成立 - 真正需要递归加锁的场景极少,优先考虑拆解逻辑或改用单锁保护整个临界段
name 参数的 Lock 在日志里就是一串内存地址,你根本分不清哪个是数据库锁、哪个是缓存锁。调试时第一件事不是加超时,而是给所有锁命名。


















