一:背景

1. 讲故事

这篇文章源自于分析一些疑难dump的思考而产生的灵感,在dump分析中经常要寻找的一个答案就是如何找到死亡线程的生前都做了一些什么?参考如下输出:


0:001> !t
ThreadCount: 22
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 20
Hosted Runtime: no
Lock
ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 3a74 00efb368 2a020 Preemptive 02F2AF48:00000000 00ec2fa0 1 MTA
5 2 6758 00f07a48 2b220 Preemptive 00000000:00000000 00ec2fa0 0 MTA (Finalizer)
XXXX 3 0 00f31df0 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 4 0 00f34080 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 5 0 00f363a8 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 6 0 00f372e8 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 7 0 00f39f80 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 8 0 00f3cbd0 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 9 0 00f3d128 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 10 0 00f40630 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 11 0 00f43310 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 12 0 00f42db8 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 13 0 00f49180 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 14 0 00f4a228 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 15 0 00f53a28 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 16 0 00f56598 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 17 0 00f59180 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 18 0 00f59b28 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 19 0 00f5e8a0 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 20 0 00f5f248 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 21 0 00f63fc0 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)
XXXX 22 0 00f66b50 1039820 Preemptive 00000000:00000000 00ec2fa0 0 Ukn (Threadpool Worker)

前面的 XXXX 代表线程已死亡,那谁能告诉我 ID=22 的线程生前执行了什么代码呢?其实去年我写了一篇如何用 WinDbg 去寻找程序中的短命线程。 TTD 专题 (第一篇):C# 那些短命线程都在干什么?

虽然可以用 WinDbg 的 TTD 来解决,但也有很多的限制,诸如:

  • 生产环境不能安装 windbg 或者 安装不上
  • 不能对生产程序进行附加

所以这两点也制约了 TTD 的强大威力,那有没有轻量级以及无侵入的方式洞察呢?最近在看 perfview 的文档,发现完全可以使用内核中Thread 的 ETW相关事件来搞定。

二:Thread 的ETW事件

1. 使用 Thread 的短命线程

如果死亡线程背后没有标记 Threadpool Worker 的话,那就说明是代码自己用 new Thread 创建出来的线程,这种比较简单,观察 Windows Kernel/Thread/Start 或者 Microsoft-Windows-DotNETRunning/Thread/Creating 的ETW事件即可。

接下来写一段简单的案例代码:


internal class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 5000; i++)
{
Test1();
} Console.ReadLine();
}
public static int index = 1; public static void Test1()
{
new Thread(() => { Test2(); }).Start();
} public static void Test2()
{
Thread.Sleep(10); var i = 10;
var j = 20; var sum = i + j; Console.WriteLine($"i={index++},sum={sum}");
}
}

代码非常简单,用 new Thread 创建了一个短命线程,接下来打开 Perfview 使用默认配置,完整的 Command 命令如下:


PerfView.exe "/DataFile:PerfViewData.etl" /BufferSizeMB:256 /StackCompression /CircularMB:500 /ClrEvents:GC,Binder,Security,AppDomainResourceManagement,Contention,Exception,Threading,JITSymbols,Type,GCHeapSurvivalAndMovement,GCHeapAndTypeNames,Stack,ThreadTransfer,Codesymbols,Compilation /NoGui /NoNGenRundown /Merge:True /Zip:True collect

程序跑完后可以用 WinDbg 的 !t 去看看凌乱现场,可以发现有大量的 XXX 线程。


0:008> !t
ThreadCount: 1386
UnstartedThread: 0
BackgroundThread: 2
PendingThread: 0
DeadThread: 1383
Hosted Runtime: no
Lock
DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 4114 02CAC9C8 2a020 Preemptive 0559F108:0559FFEC 02c9c488 -00001 MTA
6 2 31b4 02CBA5F0 2b220 Preemptive 00000000:00000000 02c9c488 -00001 MTA (Finalizer)
XXXX 3 0 02CCEC48 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 694 0 116C5B18 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 695 0 116C0578 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 696 0 116C1250 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 697 0 116BF8A0 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 698 0 116C5F60 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 699 0 116C38D8 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 700 0 116C74C8 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
...
XXXX 1380 0 115097C0 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 1381 0 115079C8 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 1382 0 1150B170 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 1383 0 1150AD28 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 1384 0 11508258 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
XXXX 1385 0 11505BD0 39820 Preemptive 00000000:00000000 02c9c488 -00001 Ukn
7 1386 6114 1150CF68 20220 Preemptive 055A07A8:055A1FEC 02c9c488 -00001 Ukn

收集一会之后停止收集,选中Thread的内核事件 Thread/Start,截图如下:

从卦中可以看到有大量的 Start 事件,我们挑选其中一个观察下线程栈,右键 Open Any Stacks 看看到底是什么代码发出了这个 ETW 事件,截图如下:

从卦中可以清晰的看到,原来是 Main() -> Test1() 方法创建的哈,终于水落石出。

2. 使用 ThreadPool 的短命线程

在真实场景中也有很多代码是用 ThreadPool 创建出来的短命线程,这种短命线程其实有一个特点,那就是曾经有大量的任务进队列,导致 ThreadPool 被迫生成很多的线程来应付,当任务全部被消灭后,ThreadPool 就会把那些被迫生成的线程全部给裁掉

卸磨杀驴,真的好像我们的职场/(ㄒoㄒ)/~~。

所以突破点就是统计下 ThreadPoolEnqueueWork 事件,有了思路之后修改下测试代码。


public static void Test1()
{
Task.Run(() => { Test2(); });
}

这里有一个注意点,程序跑完之后还要稍等一两分钟,就是让ThreadPool把多余的Thread给灭掉,用 windbg 观察到的效果图就是 讲故事 那一节的,停止 perfview 收集后,寻找 ThreadPoolEnqueueWork 事件,截图如下:

从卦中可以看到有大量的 ThreadPoolEnqueueWork 事件,接下来可以选择右键菜单 Save View as Excel 导出到 Excel 中,然后对 Time Msec 进行分组排序,看下哪一个时间段有大量的任务进队列,指标高的时间段自然就是重点怀疑的。

这里要说一点 Time MSecTrace Start Time 基础上的毫秒级偏移值。

举个例子: 4377.032 (4.37s) + 15:56:25.566 = 15:56:29.866

有了这些概念之后,找到问题区域的进队任务,观察下调用栈,大概率也能找到问题,从调用栈来看,原来是 Test1() 所致哈。。。 截图如下:

三:总结

相比WinDbg TTD的重模式,Perfiew真的很轻,而且无侵入性,这两个工具真的是珠联璧合,相得益彰。

PerfView专题 (第十四篇): 洞察那些 C# 代码中的短命线程的更多相关文章

  1. PerfView专题 (第十二篇):对 C# 下的 SDK 类库进行监控(大结局)

    一:背景 本篇是我们系列文章的最后一篇,前面的文章中大多是在 CLR Runtime 以及 OS 层面进行监控来发现各种可疑的程序问题,除了这两个层面,其实我们还可以对 SDK 中一些类进行洞察,比如 ...

  2. 解剖SQLSERVER 第十四篇 Vardecimals 存储格式揭秘(译)

    解剖SQLSERVER 第十四篇    Vardecimals 存储格式揭秘(译) http://improve.dk/how-are-vardecimals-stored/ 在这篇文章,我将深入研究 ...

  3. 第十四篇 Integration Services:项目转换

    本篇文章是Integration Services系列的第十四篇,详细内容请参考原文. 简介在前一篇,我们查看了SSIS变量,变量配置和表达式管理动态值.在这一篇,我们使用SQL Server数据商业 ...

  4. Python之路【第十四篇】:AngularJS --暂无内容-待更新

    Python之路[第十四篇]:AngularJS --暂无内容-待更新

  5. 【译】第十四篇 Integration Services:项目转换

    本篇文章是Integration Services系列的第十四篇,详细内容请参考原文. 简介在前一篇,我们查看了SSIS变量,变量配置和表达式管理动态值.在这一篇,我们使用SQL Server数据商业 ...

  6. 跟我学SpringCloud | 第十四篇:Spring Cloud Gateway高级应用

    SpringCloud系列教程 | 第十四篇:Spring Cloud Gateway高级应用 Springboot: 2.1.6.RELEASE SpringCloud: Greenwich.SR1 ...

  7. SpringBoot第二十四篇:应用监控之Admin

    作者:追梦1819 原文:https://www.cnblogs.com/yanfei1819/p/11457867.html 版权声明:本文为博主原创文章,转载请附上博文链接! 引言   前一章(S ...

  8. Egret入门学习日记 --- 第十四篇(书中 5.4~5.6节 内容)

    第十四篇(书中 5.4~5.6节 内容) 书中内容: 总结 5.4节 内容重点: 1.如何编写自定义组件? 跟着做: 重点1:如何编写自定义组件? 文中提到了重要的两点. 好,我们来试试看. 第一步, ...

  9. Spring Cloud第十四篇 | Api网关Zuul

    ​ 本文是Spring Cloud专栏的第十四篇文章,了解前十三篇文章内容有助于更好的理解本文: Spring Cloud第一篇 | Spring Cloud前言及其常用组件介绍概览 Spring C ...

  10. “全栈2019”Java多线程第三十四章:超时自动唤醒被等待的线程

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

随机推荐

  1. Gin框架快速入门

    github地址: https://github.com/gin-gonic/gin 初体验 安装: $ go get -u github.com/gin-gonic/gin 简单实例: packag ...

  2. 回车,换行,转义字符“\r”,“\n”是什么关系?

    1."回车"这个名词的来历. 关于"回车键"的来历,还得从机械英文打字机说起.在机械英文打字机上,有一个部件叫"字车",每打一个字符(原为单 ...

  3. 使用Vue.js实现文字跑马灯效果

    实现文字跑马灯效果,首先用到 substring()截取 和 setInterval计时器 clearInterval()清除计时器 效果如下: 实现代码如下: <!DOCTYPE html&g ...

  4. Analysis of Variance 方差分析

    title: "Analysis of Variance" author: '01' date: "2022-11-23" output: html_docum ...

  5. Node.js入门学习笔记

    NodeJs是js的运行时,意味着可以在浏览器外运行js.可以使用nodejs来构建服务器端应用.CLI应用.Web API,甚至用electron构建桌面端应用. 使用nvm来管理node版本. 在 ...

  6. 对dubbo的DubboReference.check的参数进行剖析

    背景 在使用dubbo的时候,发现当消费者启动的时候,如果提供者没有启动,即使提供者后来启动了,消费者也调不通提供者提供的接口了. 注册中心使用都是nacos dubbo版本是3.0.4 例子 接口 ...

  7. RTSP&IGMP详解

                                                                                 RTSP协议 一.概述 1)RTSP(Real ...

  8. Mac 下 brew安装慢的问题

    brew默认源使用的是github,可以设置环境变量达到切换源的效果,见官网: https://github.com/Homebrew/install export HOMEBREW_BREW_GIT ...

  9. 【leetcode】258. 各位相加

    [leetcode]258. 各位相加 C++解法: class Solution { public: int addDigits(int num) { string s;//用来将num转换成字符串 ...

  10. 【Azure 存储服务】使用 AppendBlobClient 对象实现对Blob进行追加内容操作

    问题描述 在Azure Blob的官方示例中,都是对文件进行上传到Blob操作,没有实现对已创建的Blob进行追加的操作.如果想要实现对一个文件的多次追加操作,每一次写入的时候,只传入新的内容? 问题 ...