快速确认线程死锁需先观察现象:多线程长期停在lock.acquire()、rlock.acquire()或queue.get()且cpu占用极低;再用kill -usr1(linux/macos)或sys._current_frames()打印线程栈,重点检查停在acquire/wait/__enter__的线程。

怎么快速确认是不是线程死锁
Python 里线程卡住不一定是死锁,可能是 I/O 阻塞、长时间计算或 time.sleep()。真要怀疑死锁,先看现象:多个线程长期停在 threading.Lock.acquire()、threading.RLock.acquire() 或 queue.Queue.get() 上,且 CPU 占用极低。
最直接的办法是发信号触发线程栈打印:
Linux/macOS 下用 kill -USR1 <pid></pid>(需 Python 启动时未忽略该信号);Windows 不支持,得靠 sys._current_frames() 手动抓。
更稳的实操是加一句调试钩子:
import threading, sys, traceback
def dump_threads():
for thread_id, frame in sys._current_frames().items():
print(f"Thread {thread_id}:")
traceback.print_stack(frame, limit=5)
在疑似卡住时调用它,重点看哪些线程停在 acquire、wait 或 __enter__ 上。
常见死锁模式和对应修复 死锁不是玄学,基本就那几类写法,改起来也快:
• 嵌套锁顺序不一致:
线程 A 先 lock1 再 lock2,线程 B 先 lock2 再 lock1 → 必现死锁。
→ 统一加锁顺序,比如按 id(lock1) 判断谁先 acquire。
• RLock 误用:RLock 允许同一线程重复 acquire,但若不同线程交叉调用且没配对 release,照样卡死。
→ 检查每个 acquire() 是否都有对应 release(),尤其异常分支里漏了 release。
• Queue + Lock 混用不当:
一边用 queue.get(block=True) 等数据,另一边用锁保护队列操作却忘了 notify 或 put —— 实际是逻辑卡点,不是锁本身问题。
→ 改用 queue.put_nowait() + try/except queue.Full 显式处理,避免无限等。
用 threading.settrace() 监控锁行为(慎用)
这不是日常手段,但排查疑难死锁很管用:给所有线程装上 trace 函数,记录每次 acquire 和 release。
要点:
立即学习“Python免费学习笔记(深入)”;
• 只在 debug 模式启用,性能损耗大,会拖慢 5–10 倍;
• trace 函数必须轻量,别 print,改用 logging.debug 写文件;
• 关键是记下线程名、锁对象 id、调用栈前两层(用 frame.f_code.co_name 和 frame.f_lineno);
• 死锁发生后,搜日志里“acquire”但没对应“release”的锁 id,再查哪些线程在等它。
为什么 logging.getLogger().addHandler() 会隐式触发死锁
这个坑很多人踩过:多线程环境下,第一次调用 logging.info() 会懒加载 logger 层级结构,内部用了模块级锁 _lock;如果此时另一个线程正持有该锁又去调 threading.Lock.acquire(),而前者又反过来等那个 Lock —— 就串起来了。
表现就是:程序启动后某次 log 调用突然卡死,堆栈停在 logging/__init__.py 的 _acquireLock;
解决办法只有两个:
• 启动时主线程先执行一次 logging.debug("init"),提前初始化锁;
• 或彻底避开 logging 在锁临界区里调用 —— 把日志内容先拼好,出锁后再 logging.info(msg)。








