windbg 分析线程死锁

整套ASP.NET线程卡死分析步骤总结(标准化排查套路)

一、第一步:宏观看整体指标 !tp

  1. 查看线程池Worker总数、Running/Idle、排队队列;
  2. 特征:Running=总线程、Idle=0、队列积压几千 → 大批量线程永久卡死不释放。
  3. 初步结论:不是瞬时并发高,是线程泄漏卡死。

二、第二步:排查托管锁 !syncblk

  1. 看每条:MonitorHeld=占用数、有无等待的Waiter线程
  2. ① 有大量等待线程 = 多lock互相争抢,传统Monitor死锁
  3. ② 只有占用、无等待线程 = 线程拿着锁卡在业务代码,锁是副产品,不是元凶(你的场景)

三、第三步:抽卡死线程栈(从syncblk里提取OS-TID)

~~[0xTID]s;!clrstack 打印单个卡死线程托管堆栈:

  1. 栈出现:ManualResetEventSlim.Wait → Task.InternalWait → Task.Wait()
    → 锁定:同步.Wait()阻塞异步任务,上下文死锁。
  2. 向上追溯业务类:定位出事方法(ServiceDiscovery、MatrixHelper、HttpModule、Controller)。

四、第四步:结合代码解释「偶发正常、随机卡死」

  1. 命中缓存/连接复用:GetAsync同步执行完毕,返回已完成Task,await不放线程,全链路顺行,正常释放;
  2. 真实跨网IO:返回未完成Task,await释放工作线程;IO完成回调要切回AspNetSynchronizationContext,但主线程被.Wait()占死 → 闭环死锁、线程永久滞留。

五、第五步:区分两种死锁、落地修复

  1. 传统lock死锁:A拿锁1等锁2、B拿锁2等锁1 → 改锁顺序、缩小lock范围;
  2. ASP上下文死锁(本次故障) :同步.Wait()/Result + await无ConfigureAwait(false)
    修复二选一:
    • 方案1:全链路改成async/await,删除所有.Wait()
    • 方案2:所有await xxx.ConfigureAwait(false),切断回调绑定请求上下文。

精简口诀

!tp看池子满不满,!syncblk辨锁死还是卡死,切线程栈找Wait,缓存决定偶发好坏