用 py-spy record -p pid -o flame.svg 直接抓取进程调用栈,重点关注停在 lock.acquire、queue.get、join 等阻塞调用的线程,可精准定位死锁点。

pytest运行卡住不动,怎么快速定位死锁点
直接看进程状态比猜更可靠。先用 ps aux | grep pytest 找到主进程 PID,再执行 kill -3 PID(Linux/macOS)或 py-spy record -p PID -o flame.svg(推荐),它会输出当前所有线程的完整调用栈。重点关注处于 Waiting for condition、acquire、join 或长时间停在 threading.Lock.acquire / queue.get 的线程。
常见诱因包括:
- 测试函数里手动调用了
threading.Thread.join()但没设 timeout,且被测线程内部又等了另一个未释放的锁 - 使用了
concurrent.futures.ThreadPoolExecutor但没调用shutdown(wait=True),导致 pytest 主线程在 exit 阶段阻塞在 executor 的 weakref 回收逻辑中(Python 3.9+ 更明显) -
pytest-xdist下多个 worker 同时访问同一本地文件(如 SQLite 文件、临时目录),而没加文件锁或隔离路径
给单个测试加超时,但不干扰 fixture 生命周期
用 pytest-timeout 插件最稳妥,它在信号层介入,不依赖测试代码配合。安装后加配置:
addopts = --timeout=30 --timeout-method=signal
注意 --timeout-method=signal(默认)比 thread 更可靠:后者靠另起监控线程抛异常,若被测代码已禁用线程(如 threading.settrace)或卡死在 C 扩展里就无效;前者用 SIGALRM 强制中断,但 Windows 不支持。
若必须用装饰器方式(如只对特定测试),避免用 @pytest.mark.timeout(需额外插件),改用标准库:
import signal<br>def timeout_handler(signum, frame):<br> raise TimeoutError("test hung")<br><br>def test_something():<br> signal.signal(signal.SIGALRM, timeout_handler)<br> signal.alarm(5) # 5秒后触发<br> try:<br> # your code here<br> finally:<br> signal.alarm(0) # 清除定时器
关键点:必须在 try/finally 中调用 signal.alarm(0),否则超时后信号持续挂起,后续测试全崩。
仅使用 Python 将 Claw Code 虚拟化框架与 OpenClaw 集成,提供 184 种工具和 200 多条命令,用于安全分析和代码质量...
fixture 中启动后台线程,为什么 pytest 退出时卡住
根本原因是 pytest 默认等待所有非守护线程结束才退出。如果你在 setup 或 fixture 中起了 threading.Thread(target=...),又没设 daemon=True,pytest 就会卡在 thread.join()。
正确做法分场景:
- 纯测试辅助线程(如 mock server):显式设
daemon=True,并确保它不持有关键资源(如数据库连接、文件句柄) - 需主动控制生命周期的线程:用 fixture 的
yield+thread.stop()模式,但注意 Python 没原生stop(),得自己实现退出标志位(如threading.Event) - 涉及 asyncio 的:别混用
threading和asyncio.run(),优先用asyncio.create_task()+asyncio.wait_for(),pytest-asyncio 插件能自动处理事件循环关闭
一个典型错误是写 thread = threading.Thread(target=run_server); thread.start(); yield; thread.join() —— 若 run_server 内部阻塞在 socket.accept() 且没响应中断,join() 就永远等下去。
复现不稳定死锁,怎么固定测试顺序和并发行为
随机性是调试死锁的最大敌人。先用 --randomly-seed=12345(需 pytest-randomly)固定执行顺序,再用 -n0 关掉 xdist 并发,排除多 worker 干扰。
进一步控制环境:
- 强制单线程:启动时加
PYTHONPATH=.和export OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1(防科学计算库偷偷开线程) - 禁用缓存:加
--cache-clear,避免旧的.pytest_cache里残留锁状态 - 检查日志:在
conftest.py里加全局logging.basicConfig(level=logging.DEBUG),重点观察threading、queue、concurrent.futures相关模块的 debug 日志
真正难搞的是那些依赖系统调度时序的锁竞争——这时候得上 py-spy 抓多个时间点的栈快照对比,看哪个线程总在同一个锁上反复等待。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










