整套ASP.NET线程卡死分析步骤总结(标准化排查套路)
一、第一步:宏观看整体指标 !tp
- 查看线程池Worker总数、Running/Idle、排队队列;
- 特征:Running=总线程、Idle=0、队列积压几千 → 大批量线程永久卡死不释放。
- 初步结论:不是瞬时并发高,是线程泄漏卡死。
二、第二步:排查托管锁 !syncblk
- 看每条:
MonitorHeld=占用数、有无等待的Waiter线程 - ① 有大量等待线程 = 多lock互相争抢,传统Monitor死锁
- ② 只有占用、无等待线程 = 线程拿着锁卡在业务代码,锁是副产品,不是元凶(你的场景)
三、第三步:抽卡死线程栈(从syncblk里提取OS-TID)
~~[0xTID]s;!clrstack 打印单个卡死线程托管堆栈:
- 栈出现:
ManualResetEventSlim.Wait → Task.InternalWait → Task.Wait()
→ 锁定:同步.Wait()阻塞异步任务,上下文死锁。 - 向上追溯业务类:定位出事方法(ServiceDiscovery、MatrixHelper、HttpModule、Controller)。
四、第四步:结合代码解释「偶发正常、随机卡死」
- 命中缓存/连接复用:
GetAsync同步执行完毕,返回已完成Task,await不放线程,全链路顺行,正常释放; - 真实跨网IO:返回未完成Task,
await释放工作线程;IO完成回调要切回AspNetSynchronizationContext,但主线程被.Wait()占死 → 闭环死锁、线程永久滞留。
五、第五步:区分两种死锁、落地修复
- 传统lock死锁:A拿锁1等锁2、B拿锁2等锁1 → 改锁顺序、缩小lock范围;
- ASP上下文死锁(本次故障) :同步
.Wait()/Result+await无ConfigureAwait(false)
修复二选一:- 方案1:全链路改成async/await,删除所有
.Wait(); - 方案2:所有
await xxx.ConfigureAwait(false),切断回调绑定请求上下文。
- 方案1:全链路改成async/await,删除所有
精简口诀
!tp看池子满不满,!syncblk辨锁死还是卡死,切线程栈找Wait,缓存决定偶发好坏