Python 线程死锁的形成与排查

9次阅读

死锁发生于线程中锁获取顺序不一致,如Thread_a持lock1等lock2、thread_b持lock2等lock1,导致双方永久阻塞;需固定加锁顺序、设timeout、加锁命名便于排查。

Python 线程死锁的形成与排查

死锁是怎么发生的(以 threading.Lock 为例)

死锁不是 python 特有,但在线程频繁争抢共享资源时极易触发。典型场景是两个线程各自持有一个锁,又同时去申请对方持有的锁:thread_a 持有 lock1 并等待 lock2thread_b 持有 lock2 并等待 lock1——双方永远卡住。

关键点在于:锁的获取顺序不一致、未设置超时、锁粒度不合理。

  • 常见错误现象:threading.Thread 启动后程序无响应,CPU 占用低,Ctrl+C 无法中断(因主线程也在等锁)
  • 使用场景:多线程更新全局字典、操作共享队列、数据库连接池复用
  • 避免方式:始终按固定顺序获取多个锁(如按变量名排序),或改用 threading.RLock(仅适用于单线程重入,不解决跨线程死锁)

如何快速定位死锁线程(用 threading.stack_size 和 sys._current_frames)

Python 不提供原生死锁检测,但可通过强制 dump 当前所有线程的调用来判断卡在哪个锁上。

实操建议:

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

  • 在疑似卡死时,发送 SIGUSR1linux/macOS)或用 py-spy record 工具windows 下可改用 sys._current_frames() 手动打印
  • 重点看每个线程是否停在 lock.acquire()condition.wait()queue.get() 等阻塞调用处
  • 对比多个线程的锁持有关系:谁 hold 了哪个 threading.Lock 实例?谁在等它?

示例片段(调试用):

import threading import sys import traceback  def dump_threads():     for thread_id, frame in sys._current_frames().items():         print(f"Thread {thread_id}:")         traceback.print_stack(frame, limit=5)

threading.Condition 和 queue.Queue 的隐式死锁风险

threading.Condition 依赖底层锁,若 wait() 前未正确 acquire(),或 notify() 后未及时 release(),会导致等待线程永远挂起。同理,queue.Queueget() / put()maxsize 设为 0 或过小时,可能因生产者/消费者节奏不匹配而集体阻塞。

  • 常见错误:在 with condition: 块外调用 condition.notify(),导致通知丢失
  • 参数差异:queue.Queue(maxsize=0) 表示无限队列,但 maxsize=1 且生产者未消费时,第二个 put() 就会阻塞
  • 性能影响:过度依赖 Condition.wait(timeout=...) 而不检查条件变量本身,可能掩盖逻辑缺陷

用 timeout 参数和 try/except 防御性加锁

所有阻塞式锁操作都应设 timeout,否则一旦逻辑出错,死锁就不可逆。

  • lock.acquire(timeout=2) 返回 False 而非无限等待,便于记录日志并主动退出
  • queue.Queue.get(timeout=1)queue.Queue.put(timeout=1) 同样适用
  • 注意:timeout 是浮点秒数,设为 0 表示非阻塞(立即返回 True/False 或抛 queue.Empty/queue.Full
  • 容易被忽略的是:超时后必须显式处理“未拿到锁”的状态,比如跳过后续操作、重试或降级为单线程执行

死锁排查最耗时的环节往往不是发现现象,而是确认哪几个线程在互相等待哪几个锁实例——尤其当锁来自不同模块、命名不清晰时。建议给每个 threading.Lock 实例加可读的 __name__ 属性或注释,方便 dump 时识别。

text=ZqhQzanResources