WinDbg 分析 .NET Dump 线程锁问题

在定位 .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 分析场景。