一:背景

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. 在.NET 6.0中自定义接口路由

    大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进. 在本文中,我们将讨论ASP.NET Core中的新路由.我们将了解什么是接口(en ...

  2. ROS用hector创建地图

    ROS用hector创建地图 连接小车 ssh clbrobot@clbrobot 激活树莓派 roslaunch clbrobot bringup.launch 打开hector_slam 重新开终 ...

  3. 区块链——Lab2

    区块链的典型数据结构 比特币:UTXO模型,以交易后找零为中心 ETH:Account 模型,以账户余额为中心(就是账户的形式) 区块链交易 用户发起交易 矿工验证交易(能够得到 区块奖励) 验证成功 ...

  4. 简单实用Ecplise常用快捷键

    简单实用Eclipse常用快捷键 用了Eclipse两年了,简单总结下目前我经常使用的快捷键!!! 1. Ctrl+Shift+R 功能:打开资源,这组快捷键可以让你打开你的工程中的任何一个文件 操作 ...

  5. 前端js几种加密/解密方法

    https://www.jianshu.com/p/4c236d83ea04 https://blog.csdn.net/guxingsheng/article/details/84451573 vu ...

  6. Linux期末佛脚指南

    Linux的期末佛脚复习 常用文件操作命令 touch (创建文件) cat (查看文件内容) head (查看文件开头) tail (查看文件结尾) cp (复制文件) mv (移动文件) ls ( ...

  7. 【Linux】sed文本处理及软件管理

    软件管理 1.编译安装http2.4,实现可以正常访问 安装编译相关工具包 root@mirror-centos8-p11 ~]# yum install gcc make autoconf apr- ...

  8. 2021-12-02:给定一个字符串str,和一个正数k。 返回长度为k的所有子序列中,字典序最大的子序列。 来自腾讯。

    2021-12-02:给定一个字符串str,和一个正数k. 返回长度为k的所有子序列中,字典序最大的子序列. 来自腾讯. 答案2021-12-02: 单调栈.先进来的元素大,后进来的元素小. 时间复杂 ...

  9. 解决:Error: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。

    启动django应用时报如下错误:Error: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试. 1.首先退出酷狗音乐再试试 2.是8000端口被其他程序占用了, ...

  10. pages.json 文件:globalStyle 全局配置

    globalStyle 用于设置应用的状态栏.导航条.标题.窗口背景色等. 属性 类型 默认值 描述 平台差异说明 navigationBarBackgroundColor HexColor #F7F ...