一:背景

1.讲故事

在分析的众多dump中,经常会遇到各种奇葩的问题,仅通过dump这种快照形式还是有很多问题搞不定,而通过 perfview 这种粒度又太粗,很难找到问题之所在,真的很头疼,比如本篇的 短命线程 问题,参考图如下:

我们在 t2 时刻抓取的dump对查看 短命线程 毫无帮助,我根本就不知道这个线程生前执行了什么代码,为什么这么短命,还就因为这样的短命让 线程池 的线程暴增。

为了能尽最大努力解决此类问题,武器库中还得再充实一下,比如本系列要聊的 Time Travel Debug,即时间旅行调试。

二: Time Travel Debug

1. 什么是 时间旅行调试

如果说 dump 是程序的一张照片,那 TTD 就是程序的一个短视频,很显然短视频的信息量远大于一张照片,因为视频记录着疑难杂症的前因后果,参考价值巨大,简直就是银弹般的存在。

三:案例演示

1. 参考代码

这是我曾经遇到的一个真实案例,在没有 TTD 的协助下最终也艰难的找到了问题,但如果有 TTD 的协助简直就可以秒杀,为了方便说明,先上一个测试代码。


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

程序跑完之后,我们抓一个dump文件,输出如下。


0:000> !t
ThreadCount: 20
UnstartedThread: 0
BackgroundThread: 7
PendingThread: 0
DeadThread: 13
Hosted Runtime: no
Lock
DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 12f8 00C4AF20 80030220 Preemptive 03C3FFAC:03C40000 00c462f8 -00001 Ukn
6 2 6a70 00C5BBD8 2b220 Preemptive 03C521B8:03C53FE8 00c462f8 -00001 MTA (Finalizer)
XXXX 4 0 00C9FEB0 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
7 5 6694 00CA0990 302b220 Preemptive 03C40314:03C41FE8 00c462f8 -00001 MTA (Threadpool Worker)
XXXX 6 0 00CB53B8 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
XXXX 7 0 00CB5958 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
XXXX 8 0 00CB4338 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
XXXX 9 0 00CB4C58 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
XXXX 10 0 08879278 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
8 11 5d10 08879E90 102b220 Preemptive 03C2AC2C:03C2BFE8 00c462f8 -00001 MTA (Threadpool Worker)
XXXX 12 0 0887D1F8 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
XXXX 13 0 0887C0D8 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
XXXX 14 0 0887AB70 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
XXXX 15 0 0887B400 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
XXXX 16 0 0887D640 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
XXXX 17 0 0887A728 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
9 18 5658 0887C520 102b220 Preemptive 03C46684:03C47FE8 00c462f8 -00001 MTA (Threadpool Worker)
10 19 564 0887C968 102b220 Preemptive 03C4A664:03C4BFE8 00c462f8 -00001 MTA (Threadpool Worker)
XXXX 20 0 0887AFB8 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)
11 3 547c 0887A2E0 2b220 Preemptive 03C50008:03C51FE8 00c462f8 -00001 MTA

2. 为什么会有很多短命线程

windbg 的输出看有很多的 XXX,那原因是什么呢? 还得先观察下代码,可以看到代码会给 ThreadPool 分发 100 次任务,每个任务也就 1s 的运行时间,这样的代码会造成 ThreadPool 的工作线程处理不及继而会产生更多的工作线程,在某一时刻那些 Sleep 后的线程又会规模性唤醒,ThreadPool 为了能够平衡工作者线程,就会灭掉很多的线程,造成 ThreadPool 中的暴涨暴跌现象。

因果关系是搞清楚了,但对于落地是没有任何帮助的,比如线程列表倒数第二行已死掉的线程:


XXXX 20 0 0887AFB8 1039820 Preemptive 00000000:00000000 00c462f8 -00001 Ukn (Threadpool Worker)

你是没法让它起死回生的,对吧?这时候就必须借助 TTD 录制一个小视频。

3. TTD 录制

录制非常简单,选择 Lauch executable (advanced) 项再勾选 Record 即可,截图如下:

等程序执行完了或者你觉得时机合适再点击 Stop and Debug 停止录制,截图如下:

稍等片刻,你会得到如下三个文件。

  1. ConsoleApp101.run 录制文件
  2. ConsoleApp101.idx 录制的索引文件
  3. ConsoleApp101.out 日志文件

4. 分析思路

  1. 找到 tid=20 的 OSID 线程ID

因为此时的 tid=20 的 OSID 已经不存在了,所以用 !tt 在时间刻度上折半查找 OSID 存在的 position。


0:007> !tt 94
Setting position to 94% into the trace
Setting position: 396DB:0
(5ac8.20): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 396DB:0
eax=00be602c ebx=00c7c2b0 ecx=00be6028 edx=0024e000 esi=00be6028 edi=00000000
eip=77d8e925 esp=07acf1c8 ebp=07acf1c8 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
ntdll!RtlEnterCriticalSection+0x15:
77d8e925 f00fba3000 lock btr dword ptr [eax],0 ds:002b:00be602c=ffffffff
0:007> !t
ThreadCount: 20
UnstartedThread: 0
BackgroundThread: 19
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
Lock
DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
...
24 20 145c 0887AFB8 302b220 Preemptive 03C4C1A4:03C4DFE8 00c462f8 -00001 MTA (Threadpool Worker)

可以清楚的看到原来是 OSID =145cWindbgID=24 有了这个信息不代表此时它正在执行托管方法,所以我们还需要找到这个 145c 是何时出生的?

  1. 找到当前视频中所有的 ThreadCreated 事件。

可以在 Events 输出信息中检索 id=0x145c 的线程出生信息。


0:024> dx -r2 @$curprocess.TTD.Events.Where(t => t.Type == "ThreadCreated").Select(t => t.Thread).Where(t=>t.Id==0x145c).Select(t=>t)
@$curprocess.TTD.Events.Where(t => t.Type == "ThreadCreated").Select(t => t.Thread).Where(t=>t.Id==0x145c).Select(t=>t)
[0x0] : UID: 27, TID: 0x145C
UniqueId : 0x1b
Id : 0x145c
Lifetime : [38B21:0, 3BB45:0]
ActiveTime : [38B6A:0, 3BB45:0]
GatherMemoryUse [Gather inputs, outputs and memory used by a range of execution within a thread]

从输出中可以看到, Lifetime 表示这个线程的一生, ActiveTime 则是从线程的Start处开始的,画个图如下:

接下来将进度条调到 !tt 38B21:0 处,那如何看代码进入到托管方法中呢?这个就得各显神通,我知道的有这么几种。

  1. 使用单步调试

先用 !tt 调整大致范围,然后用 p,pc,pt,t,tc,tt 微调,比如我们这篇的 !tt 94 就能获取到 tid=20 号线程的托管部分。


0:024> !tt 94
Setting position to 94% into the trace
Setting position: 396DB:0
(5ac8.20): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 396DB:0
eax=00be602c ebx=00c7c2b0 ecx=00be6028 edx=0024e000 esi=00be6028 edi=00000000
eip=77d8e925 esp=07acf1c8 ebp=07acf1c8 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
ntdll!RtlEnterCriticalSection+0x15:
77d8e925 f00fba3000 lock btr dword ptr [eax],0 ds:002b:00be602c=ffffffff
0:007> ~24s
eax=00000000 ebx=0b1bfab8 ecx=00000000 edx=00000000 esi=00000001 edi=0b1bfab8
eip=77dc196c esp=0b1bfa78 ebp=0b1bfadc iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
ntdll!NtDelayExecution+0xc:
77dc196c c20800 ret 8
0:024> !clrstack
OS Thread Id: 0x145c (24)
Child SP IP Call Site
0B1BFB50 77dc196c [HelperMethodFrame: 0b1bfb50] System.Threading.Thread.SleepInternal(Int32)
0B1BFBBC 07b90694
0B1BFBD0 03b99078 ConsoleApp1.Program.Test()
0B1BFC04 03b98a03 ConsoleApp1.Program+c.b__0_0()
0B1BFC10 07b9065d System.Threading.Tasks.Task.InnerInvoke() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2387]
0B1BFC1C 07b900cd System.Threading.Tasks.Task+c.<.cctor>b__272_0(System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2375]
0B1BFC24 07b90047 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs @ 268]
0B1BFC54 07b907d2 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2337]
0B1BFCB8 03b9ff34 System.Threading.Tasks.Task.ExecuteEntryUnsafe(System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2277]
0B1BFCC8 070f7a36 System.Threading.ThreadPoolWorkQueue.Dispatch()
0B1BFD24 070ff222 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs @ 63]
0B1BFDB0 070e6545 System.Threading.Thread.StartCallback() [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs @ 105]
0B1BFF04 0307b9cf [DebuggerU2MCatchHandlerFrame: 0b1bff04]
  1. 对 compileMethod 方法下断点

C# 的一个特性就是很多方法都是由 JIT 动态编译的,因为很多方法都是未编译,所以遇到编译事件的时候执行流很大概率就在托管层。


0:024> bp clrjit!CILJit::compileMethod
0:024> g
Breakpoint 0 hit
Time Travel Position: 3939B:12E9
eax=07acf8c8 ebx=07acf9d4 ecx=503d34b0 edx=00000000 esi=502bca30 edi=503d34b0
eip=502bca30 esp=07acf784 ebp=07acf9c8 iopl=0 nv up ei ng nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000282
clrjit!CILJit::compileMethod:
502bca30 55 push ebp

TTD 专题 (第一篇):C# 那些短命线程都在干什么?的更多相关文章

  1. asp.net signalR 专题—— 第一篇 你需要好好掌握的实时通讯利器

    一:背景 我们知道传统的http采用的是“拉模型”,也就是每次请求,每次断开这种短请求模式,这种场景下,client是老大,server就像一个小乌龟任人摆布, 很显然,只有一方主动,这事情就没那么完 ...

  2. PerfView专题 (第一篇):如何寻找热点函数

    一:背景 准备开个系列来聊一下 PerfView 这款工具,熟悉我的朋友都知道我喜欢用 WinDbg,这东西虽然很牛,但也不是万能的,也有一些场景他解决不了或者很难解决,这时候借助一些其他的工具来辅助 ...

  3. 第一篇&nbsp;UCOS介绍

    第一篇 UCOS介绍 这个大家都知道.呵呵.考虑到咱们学习的完整性还是在这里唠叨一下.让大家再熟悉一下.高手们忍耐一下吧! uC/OS II(Micro Control Operation Syste ...

  4. java 线程池第一篇 之 ThreadPoolExcutor

    一:什么是线程池? java 线程池是将大量的线程集中管理的类,包括对线程的创建,资源的管理,线程生命周期的管理.当系统中存在大量的异步任务的时候就考虑使用java线程池管理所有的线程.减少系统资源的 ...

  5. 【JAVA并发第一篇】Java的进程与线程

    1.进程与线程 1.1.进程 进程可以看作是程序的执行过程.一个程序的运行需要CPU时间.内存空间.文件以及I/O等资源.操作系统就是以进程为单位来分配这些资源的,所以说进程是分配资源的基本单位. ( ...

  6. .net开发笔记(十三) Winform常用开发模式第一篇

    上一篇博客最后我提到“异步编程模型”(APM),之后本来打算整理一下这方面的材料然后总结一下写篇文章与诸位分享,后来在整理的过程中不断的延伸不断地扩展,发现完全偏离了“异步编程”这个概念,前前后后所有 ...

  7. RabbitMQ学习总结 第一篇:理论篇

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  8. Python开发【第一篇】:目录

    本系列博文包含 Python基础.前端开发.Web框架.缓存以及队列等,希望可以给正在学习编程的童鞋提供一点帮助!!! Python开发[第一篇]:目录 Python开发[第二篇]:初识Python ...

  9. Asp.net原理(第一篇)

    Asp.net (第一篇) 当用户在浏览器输入一个URL地址后,浏览器会发送一个请求到服务器.这时候在服务器上第一个负责处理请求的是IIS.然后IIS再根据请求的URL扩展名将请求分发给不同的ISAP ...

随机推荐

  1. [javaweb]javaweb中HttpServletResponse实现文件下载,验证码和请求重定向功能

    HttpServletResponse web服务器接受到客户端的http请求之后,针对这个请求,分别创建一个代表请求的httpServletRequest和代表响应的HttpServletRespo ...

  2. Mybatis源码解读-配置加载和Mapper的生成

    问题 Mybatis四大对象的创建顺序? Mybatis插件的执行顺序? 工程创建 环境:Mybatis(3.5.9) mybatis-demo,参考官方文档 简单示例 这里只放出main方法的示例, ...

  3. flex 我所理解不够深刻的内容

    1.align-items属性   父元素 align-items属性定义项目在交叉轴上如何对齐. flex-start:交叉轴的起点对齐. flex-end:交叉轴的终点对齐. center:交叉轴 ...

  4. 从零开始Blazor Server(6)--基于策略的权限验证

    写这个的原因 现在BootstrapBlazor处于大更新时期,Menu组件要改为泛型模式. 本来我们的这一篇应该是把Layout改了,但是改Layout肯定要涉及到菜单,如果现在写了呢,就进入一个发 ...

  5. 5.26 NOI 模拟

    \(T1\)石子与HH与HHの取 博弈是不可能会的 \(c_i\)相等,比较显然的\(Nim,\)直接前缀异或求一下 \(a_i=1,\)区间长度对\(2\)取模 结论\(:\)黑色石子严格大于白色个 ...

  6. 如何在CDH5上部署Dolphin Scheduler 1.3.1

    点击蓝色字关注! 本篇文章大概8440字,阅读时间大约20分钟 本文记录了在CDH5.16.2集群上集成Dolphin Scheduler 1.3.1的详细流程,特别注意一下MySQL数据库的连接串! ...

  7. Debezium的基本使用(以MySQL为例)

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. GreatSQL是MySQL的国产分支版本,使用上与MySQL一致. 一.Debezium介绍 摘自官网: Debeziu ...

  8. 【MATLAB】学习记录2-数组与向量

    1-数组 A=[1,2,3;4,5,6]%创建数组 [r,c]=size(A)%返回行列数 b=size(A) c=length(A)%返回最大的维数值 2-创建数组 先创建A数组 B=zeros(2 ...

  9. Java数据类型扩展及面试题讲解

    这是关于数据类型的面试题,还是比较重要的. 1.整数拓展: 我们肯定都或多或少知道一个问题,叫进制问题.咱们常见的电脑里的二进制,然后咱们平时生活中用的是十进制,然后咱们工作的时候还有一个十六进制跟八 ...

  10. 网卡限速工具之WonderShaper

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. GreatSQL是MySQL的国产分支版本,使用上与MySQL一致. 什么是WonderShaper 如何安装Wonder ...