TTD 專題 (第一篇):C# 那些短命線程都在乾什麼?

来源:https://www.cnblogs.com/huangxincheng/archive/2022/10/06/16756970.html
-Advertisement-
Play Games

一:背景 1.講故事 在分析的眾多dump中,經常會遇到各種奇葩的問題,僅通過dump這種快照形式還是有很多問題搞不定,而通過 perfview 這種粒度又太粗,很難找到問題之所在,真的很頭疼,比如本篇的 短命線程 問題,參考圖如下: 我們在 t2 時刻抓取的dump對查看 短命線程 毫無幫助,我根 ...


一:背景

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • python爬蟲瀏覽器偽裝 #導入urllib.request模塊 import urllib.request #設置請求頭 headers=("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, l ...
  • OpenAI Gym是一款用於研發和比較強化學習演算法的工具包,本文主要介紹Gym模擬環境的功能和工具包的使用方法,並詳細介紹其中的經典控制問題中的倒立擺(CartPole-v0/1)問題。最後針對倒立擺問題如何建立控制模型並採用爬山演算法優化進行了介紹,並給出了相應的完整python代碼示例和解釋。要... ...
  • 作者:IT王小二 博客:https://itwxe.com 前面小二介紹過使用Typora+PicGo+LskyPro打造舒適寫作環境,那時候需要使用水印功能,但是小二在升級LskyPro2.x版本發現有很多不如人意的東西,遂棄用LskyPro使用MinIO結合代碼實現自己需要的圖床功能,也適合以後 ...
  • 哈嘍,兄弟們! 最近有許多小伙伴都在吐槽打工好難。 每天都是執行許多重覆的任務 例如閱讀新聞、發郵件、查看天氣、打開書簽、清理文件夾等等, 使用自動化腳本,就無需手動一次又一次地完成這些任務, 非常方便啊有木有?! 而在某種程度上,Python 就是自動化的代名詞。 今天就來和大家一起學習一下, 用 ...
  • 思路分析 登錄頁面,我們還是採用ajax的方式提交用戶數據 唯一需要學習的是如何製作圖片驗證碼! 具體的登錄頁面效果圖如下: 如何製作圖片驗證碼 推導步驟1:在img標簽的src屬性里放上驗證碼的請求路徑 補充1.img的src屬性: 1.圖片路徑 2.url 3.圖片的二進位數據 補充2:字體樣式 ...
  • 思路分析 註冊頁面需要對用戶提交的數據進行校驗,並且需要對用戶輸入錯誤的地方進行提示! 所有我們需要使用forms組件搭建註冊頁面! 平時我們書寫form是組件的時候是在views.py裡面書寫的, 但是為了接耦合,我們需要將forms組件都單獨寫在一個地方,需要用的時候導入就行! 例如,在項目文件 ...
  • 許多情況下我們需要用到攝像頭獲取圖像,進而處理圖像,這篇博文介紹利用pyqt5、OpenCV實現用電腦上連接的攝像頭拍照並保存照片。為了使用和後續開發方便,這裡利用pyqt5設計了個相機界面,後面將介紹如何實現,要點包括界面設計、邏輯實現及完整代碼。 ...
  • 在日常後端Api開發中,我們跟前端的溝通中,通常需要協商好入參的數據類型,和參數是通過什麼方式存在於請求中的,是表單(form)、請求體(body)、地址欄參數(query)、還是說通過請求頭(header)。 當協商好後,我們的介面又需要怎麼去接收這些數據呢?很多小伙伴可能上手就是直接寫一個實體, ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...