死循环表现为cpu持续90%+、线程状态running/runnable、clrstack -a显示同一行反复执行;死锁则cpu低、线程wait/blocked、dotnet-dump threads显示monitor.enter等阻塞调用。

死循环不会卡住线程栈帧里的 lock 或 WaitOne,它只会让 CPU 持续飙高、线程不阻塞、堆栈停在某一行反复执行——这点和死锁完全相反,别用查死锁那套思路去盯 clrstack。
怎么一眼区分死循环和死锁
死锁:CPU 占用低(线程全在等),线程状态是 Wait / Blocked,dotnet-dump threads 显示多个线程停在 Monitor.Enter、Mutex.WaitOne、Task.Wait;
死循环:CPU 占用持续 90%+,线程状态是 Running 或 Runnable,clrstack -a 看到同一行代码反复出现(比如 while (condition) 里没改 condition,或 for 漏了 i++);
- UI 卡死但 CPU 高 → 八成是 UI 线程进了死循环,不是死锁
- 后台服务内存缓慢上涨 + CPU 高 → 可能是异步死循环(比如
while (await SomeAsync())忘了await Task.Delay控制节奏) -
!dumpheap -stat显示大量AsyncStateMachineBox实例 → 对应 async 方法没退出,正在反复调度
WinDbg 中定位死循环的三步法
用 dotnet-dump collect -p <pid></pid> 抓完 dump 后,在 WinDbg 中:
第一步:看线程是否真在跑 —— 执行 ~*e !clrstack,找 IP(指令指针)地址不变、且调用链深度很浅的线程(比如只有一两层,全是你的方法);
第二步:确认是不是空转 —— 对可疑线程执行 ~<tid>s</tid> 切过去,再 u @rip L10 反汇编当前指令附近代码,看有没有无条件跳转回顶部的 jmp 或 je 循环结构;
第三步:结合源码定位逻辑缺陷 —— 如果有 PDB,!u <methodaddr></methodaddr> 能反查到 C# 行号;没有 PDB 就靠 !dumpmt -md <methodtable></methodtable> 查方法名,再人工比对;
- 特别注意
while (true)里漏 break、for (int i = 0; i 中 list 被内部修改导致 <code>i永远追不上Count - 异步场景下,
while (flag) { await DoWork(); }如果DoWork永远不改flag,就是典型异步死循环 - 不要依赖
!threads的 “WaitReason”,死循环线程的 WaitReason 是Unknown或空
FileStream 构造卡住?先别急着查循环
现象像死循环(CPU 不高、界面卡、ReadAllBytes 不返回),但实际是 FileStream 构造函数同步等待文件句柄释放,属于 I/O 阻塞,不是代码逻辑循环;
常见组合直接踩坑:
-
FileShare.None+ 多线程打开同一文件 → 第二个线程永远卡在new FileStream(...)构造函数里 -
FileAccess.Read+FileShare.ReadWrite→ 被 Excel、记事本等进程写锁拦截,表现和死循环一模一样 - 没加
FileOptions.Asynchronous→ReadAsync会退化成同步读,阻塞线程而非释放上下文
验证方式很简单:把文件换成本地临时路径、关掉所有可能占用它的程序,再试 —— 如果立刻恢复,就不是你代码的问题。
async/await 场景下最隐蔽的“伪死循环”
不是真循环,但行为和卡死无异:比如在 WinForms 或 ASP.NET Core 同步上下文中调用 task.Result,或 Task.Run(() => MyAsyncMethod()).Result,会导致线程干等 continuation 回来,而 continuation 又在等这个线程空出来 —— 表现为 CPU 低、线程挂起、堆栈停在 Task.GetAwaiter().GetResult();
- 这种“闭环等待”在
clrstack里能看到GetResult→InternalWait→Monitor.ObjWait链,和死锁栈很像,但只涉及一个线程 -
await foreach默认捕获同步上下文(ConfigureAwait(true)),UI 线程里直接用,第一次迭代后就卡住 - 真正该检查的是调用链顶端有没有同步等待异步操作 —— 不是看循环,是看有没有
.Result、.Wait()、.GetAwaiter().GetResult()
复杂点在于:有些死循环藏在第三方库回调里,比如事件处理中反复触发自身;有些异步死循环只在特定数据条件下触发(如空列表时 while (list.Any()) 永不退出)。得结合日志打点 + 多次 dump 对比 IP 变化才能揪出来。











