在定位 .NET 应用程序中的高 CPU 占用问题时,WinDbg 是非常强大的工具之一,尤其配合 SOS 扩展使用可以快速锁定"忙线程"或死锁等问题。
本文将基于一次实际的分析流程,演示如何一步步定位由线程锁引起的 CPU 高占用。
1. 加载 SOS 扩展(针对 .NET)
首先,我们需要加载 SOS.dll。根据你所调试的 .NET 版本不同,使用 .loadby
指令时的模块名也不同:
shell
.loadby sos clr
注意:
-
.NET Framework
使用的是clr.dll
,所以.loadby sos clr
正确; -
如果你调试的是
.NET Core
或.NET 5+
,对应模块可能是coreclr.dll
; -
可使用
lm
命令确认实际加载的模块名。
2.查看cpu占用高的线程
windbg
!runaway
这个命令显示自 WinDbg 附加后各线程的 CPU 占用时间。
3. 查看每个线程的调用栈
查看所有线程的调用栈是分析的关键一步。我们使用以下命令:
shell
~* k
这会列出所有线程的 原生调用堆栈(native stack)。
关注以下三类线程特征:
持续执行的线程(高 CPU 嫌疑线程)
栈顶函数是业务逻辑方法、算法处理、循环等,说明该线程在"忙",是最需要关注的对象。
卡在等待(阻塞)状态的线程
以下函数说明线程被阻塞,可能在等待锁或资源:
-
WaitForSingleObject
-
Monitor.Enter
-
WaitOne
-
Sleep
找到等待的资源后,看正在等待什么,如果正在等待GC,则继续找谁在GC

找到在执行 GC 的线程
如果调用栈中包含以下函数,说明线程正在 GC 中:
-
clr!GCHeap::GarbageCollect
-
clr!SVR::gc_heap::gc1
-
clr!SVR::gc_heap::gc2
-
clr!SVR::gc_heap::gc3
-
clr!GCHeap::GarbageCollectGeneration
-
clr!SVR::GCHeap::GarbageCollect
-
clr!GCHeap::gc_thread_function
-
GCInterface::Collect
频繁GC会挂起线程,增加CPU消耗。

4. 分析具体线程
在上一步中,如果你发现某个线程(例如线程 28)调用栈活跃、函数栈持续变化,或者涉及 GC、锁等待,可以使用以下命令聚焦:
shell
~28s
!clrstack
这将切换到线程 28 并显示它的托管调用栈,便于你进一步确认是否存在如下情况:
-
死循环或密集计算导致高 CPU;
-
一直等待某个锁对象,导致其他线程堆积;
-
某些资源释放不及时,导致线程频繁争抢。
总结
通过上述方法,我们可以初步判断线程是否因锁或其他因素导致 CPU 占用异常。在实际排查中,掌握如下三点尤为重要:
-
先宏观查看所有线程调用栈;
-
识别忙线程 / 等待线程/ GC线程****;
-
进一步使用
!clrstack
分析托管调用栈。
这是一种稳定、高效的诊断思路,尤其适用于高 CPU 的 dump 分析场景。