Python 线程死锁的排查方法

1次阅读

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

Python 线程死锁的排查方法

怎么快速确认是不是线程死锁 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)

在疑似卡住时调用它,重点看哪些线程停在 acquirewait__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 函数,记录每次 acquirerelease

要点:

立即学习Python免费学习笔记(深入)”;

• 只在 debug 模式启用,性能损耗大,会拖慢 5–10 倍;

• trace 函数必须轻量,别 print,改用 Logging.debug 写文件;

• 关键是记下线程名、锁对象 id、调用栈前两层(用 frame.f_code.co_nameframe.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)

锁的嵌套深度、跨线程传递、和 logging/queue/condition 的交互,才是真实项目里死锁反复出现的根因。盯住 acquire/release 是否成对、是否跨线程、是否混用不同同步原语,比背原理有用得多。

text=ZqhQzanResources