程序卡在acquire()不退出基本是死锁,需通过线程堆栈定位等待关系、加超时日志暴露卡点、统一锁获取顺序(如按id升序)、慎用rlock,并优先输出锁持有与等待状态。

程序卡在 acquire() 上不退出,基本就是死锁了——它不会报错,只安静地停住。核心思路不是“等它自己好”,而是主动定位谁在等谁、为什么等不到。
看线程堆栈,确认是不是真死锁
死锁最典型的表象是多个线程长期停在 Lock.acquire() 或 RLock.acquire(),且 CPU 占用极低。别靠猜,直接抓现场:
- Linux/macOS 下执行
kill -USR1 $PID(Python 进程需未忽略该信号),会打印所有线程当前调用栈 - Windows 或无法发信号时,用
sys._current_frames()主动抓:import sys, traceback for thread_id, frame in sys._current_frames().items(): print(f"Thread {thread_id}:") traceback.print_stack(frame, limit=5) - 重点找:多个线程都停在
acquire(),且参数是同一个Lock对象(id(lock)相同);或 A 线程停在lock_a.acquire()、B 线程停在lock_b.acquire(),而日志/代码显示 A 持有lock_b、B 持有lock_a
加超时和日志,暴露卡点位置
用 with lock: 隐蔽性强,出问题难定位。换成带超时的显式调用,让死锁“当场翻车”:
- 把
with lock_a:改成if lock_a.acquire(timeout=2): ... finally: lock_a.release(),超时抛TimeoutError,立刻知道哪一步卡住 - 在每次
acquire()前和release()后打日志,必须包含:threading.current_thread().name、id(lock)、时间戳、调用位置(frame.f_lineno) - 给锁加可读名,比如
Lock(name="cache_lock"),否则日志里全是<unlocked _thread.lock object at></unlocked>,毫无意义
统一锁获取顺序,从源头堵死循环等待
90% 的死锁来自不同线程以不同顺序请求多个锁。例如函数 f() 先 lock_a 再 lock_b,而 g() 反过来——只要并发执行,就可能卡死。
- 所有地方都按固定顺序拿锁,推荐按
id()升序:locks = sorted([lock_a, lock_b], key=id) for lock in locks: lock.acquire() - 封装成工具函数,避免手误:
def acquire_all(*locks): [l.acquire() for l in sorted(locks, key=id)] - 注意:释放顺序无需严格逆序,但绝不能在已持有小
id锁时再去申请更大id锁——这等于主动破坏顺序约定
别迷信 RLock,它不解决跨线程死锁
RLock 只允许同一线程重复 acquire(),对多线程交叉等待完全无效。反而因“不报错”掩盖设计缺陷:
- 常见误区:把
mutexA = mutexB = RLock()当成“一把万能锁”,结果线程 A 拿了它去调 B,B 又去拿另一把锁,而线程 C 正相反——死锁照常发生 -
RLock的内部计数器不对外暴露,日志里看不出“当前是否已持有”,排查时比Lock更难定位 - 除非明确需要单线程递归加锁(如重入回调),否则优先用
Lock;用RLock时,务必确保所有路径的acquire()/release()成对,尤其异常分支里不能漏release()
真正麻烦的不是锁本身,而是锁之间的依赖关系——它藏在调用链深处,不靠日志和堆栈还原不出来。一旦怀疑死锁,第一反应不该是改代码,而是先让程序把“谁拿了什么、在等什么”吐出来。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











