Windows中的線程命名雜談


Windows允許您為進程中的線程指定名稱,然后調試器可以顯示這些名稱。這是一個很好的解決方案,但這是一個很好的解決方案。Windows 10 Creators更新(SetThreadDescription)中添加了一個新的線程命名API。Chrome現在使用SetThreadDescription來命名它的線程(當這個函數可用時)。chromiumrepo還包含一個工具,可以使用GetThreadDescription轉儲進程的所有線程名稱。xperf/WPA支持SetThreadDescription API——線程名稱會顯示在CPU使用率圖和通用事件圖中,這非常棒。VS 2017 15.6(更新6)和Windows應用商店版本的windbg現在支持線程名稱,包括實時調試和小型轉儲!name列現在包含您給定的任何名稱(請注意未命名的TppWorkerThread,因為Windows仍然落后)。已經被注入到線程池中,等待最早更新的線程是2019年6月。我們看看效果如何。其他需要命名的Microsoft線程,因為它們最終會出現在其他開發人員的進程中,包括:

  • DbgUiRemoteBreakin
  • CManagerImpl::_DelegateThreadProc
  • CRpcThreadCache::RpcWorkerThreadEntry

目前,在windbg的進程和線程窗口中,這些線程看起來都是相同的,並且讓完全不同的線程看起來無法區分是個壞主意。這也使得在Chrome瀏覽器進程中很難找到Chrome忘記命名的線程。

Orbit Profiler支持使用GetThreadDescription()獲取線程名稱–太棒了!

 

下面是WPA中的線程名稱示例,它使這些事件在進程和線程之間的分布更易於查看:

 

Visual Studio中的線程名稱也非常棒,尤其是現在即使在進程啟動很久之后附加線程名稱也會出現:

線程名非常棒,應該會讓Chrome調試變得簡單一點。

看看WPA中線程名有多有用!

所有線程命名問題的狀態摘要–剩下的唯一任務是Microsoft需要開始命名其線程:

示例代碼中的Const正確性:Fixed

/analyze示例代碼中的警告:已修復

調試器中的競爭條件:在VS2015更新2中修復

操作系統線程命名功能:在Windows10 Creators更新中添加(文檔稱1607/周年紀念版,但這是不正確的)

線程命名函數的工具支持:xperf/WPA、visualstudio和windbg的存儲版本都顯示線程名稱。“經典”版本的windbg通過非常明顯的命令“dx-g”顯示線程名稱@$curprocess.線程“–這里討論了更多的想法

Windows需要為自己的線程命名,尤其是它注入到其他進程中的線程,比如ntdll.dll!TppWorkerThread(應命名為ThreadPoolWorker)。這也適用於創建線程的第三方庫、注入線程的圖形驅動程序等等。查找SetThreadDescription並使用它(如果可用)。並選擇描述性但簡短的線程名稱
線程的imageNaming無疑是有幫助的。它在調試時提供了額外的上下文,可用的信息越多越好。右邊的屏幕截圖顯示了調試UIforETW時visualstudio中threads窗口中的name列。
然而,線程命名存在許多缺陷,主要是因為它只是一個調試器約定,而不是一個操作系統特性。

出現主要缺陷是因為Windows上的線程命名是通過引發異常來實現的。有一個約定,調試器應該注意異常代碼0x406D1388–是的,這只是一個沒有內在含義的任意幻數–並在相關的異常記錄中查找幻數值。調試器必須執行以下操作:
打電話給WaitForDebugEvent。如果出現異常,並且異常代碼為0x406D1388,並且參數的數目是正確的值,則重新解釋異常信息。查看該結構,如果dwType等於4096,dwFlags為零,則使用ReadProcessMemory從調試對象的內存中獲取線程名稱。
哦,從哪里開始…
這一准則的特殊性是顯而易見的。debuggee設置一組參數,然后使用RaiseException向調試器發出信號。如果調試器支持線程命名,那么它將處理這個特定的異常(ExceptionCode、NumberOfParameters、dwType和dwFlags的匹配值),然后從調試對象的內存中讀取線程名稱。無論調試器是否支持異常,調試對象都會處理異常並繼續。這是IPC的一種粗略的方法。
這種技術的好處是它存在。如果這個特性被建議作為一個操作系統的特性,那么它可能會在設計審查或規划中被捆綁幾十年。使用這種黑客技術意味着visualstudio調試器團隊(MS_VC_異常代碼背叛了他們的手)可以實現調試器端,記錄如何在客戶端調用它,讓它立即在所有版本的Windows上運行,然后重新開始工作。Windbg很容易實現了相同的特性,一切都很好。
但是,這種權宜之計給我們留下了一些問題。

不是缺陷

當我第一次在tweet上發布這些問題時,我得到了一個回復,聲稱64位構建的結構定義是錯誤的。這個機制確實很脆弱,但並沒有被打破。Win32調試API的一個限制是調試器和調試對象的位必須匹配—32位進程不能附加到64位進程。而且,只要調試器和debuggee具有相同的結構布局,那么它是什么並不重要。由於調試器和調試對象將使用相同的編譯器和相同的結構定義,因此它們將具有相同的布局,這就是所需的全部內容。
不需要使用pragma pack指令來強制執行結構的任何特定打包,但需要它們來確保兩邊都具有相同的布局。
visualstudio是32位的,它可以調試64位進程,但它通過使用IPC與64位調試器代理進行通信來實現這一點。它使用msvsmon.exe,所以64位進程的調試基本上是本地遠程調試。


小問題


我想先解決這些問題,盡管它們不是嚴重的問題。

示例代碼已很長時間沒有更新。因為它的threadName參數被聲明為char*,所以不能用constchar數組調用它,如果使用/Zc:strictStrings(非常方便)構建它,甚至不能用string常量調用它。這個小錯誤現在已經被粘貼到數千個代碼基中。我建議在代碼中修復這個問題,但是使用剪切粘貼編碼意味着這個糟糕的示例將永遠存在。現在修好了!

示例代碼觸發兩個/analyze警告。其中一個抱怨篩選器表達式是常量,另一個則抱怨\uuExcept塊為空。analyze團隊應該將SetThreadName文檔視為這些構造通常不是bug的證據,文檔團隊應該添加必要的警告抑制雜注。仍然相關的示例代碼應該在高警告級別下編譯干凈。現在修好了!

大問題

所有這些大問題都源於設計的一個后果:如果在引發異常時沒有調試器在監聽,那么線程名稱將永遠丟失。
如果調試程序在線程命名完成后附加,則調試器將不知道它錯過了這些異常。無法要求調試人員再次引發異常。我想調試人員可以每隔幾秒鍾重命名一次線程,但這充其量是一種減少危害的策略,如果在進程崩潰時附加調試器,它仍然會完全失敗。當前的策略意味着大多數Windows錯誤報告崩潰都沒有線程名。
事實證明,調試器並不是唯一可以從了解線程名稱中獲益的工具。探查器,如Windows Performance Toolkit(xperf)將通過使用thread name列得到極大的增強,按線程名分組將是非常棒的。但是,作為調試器附加到被分析的進程是一個糟糕的主意,所以Windows性能分析器(WPA)無法獲取這些信息。
另外,兩個主要的Windows調試器都有一個可以避免的爭用條件,這會導致SetThreadName頻繁地靜默失敗。如果線程創建事件在線程命名異常之前到達,則這兩個調試器似乎只命名線程。如果您創建了一個線程,然后立即從creator線程中命名它,那么在線程開始運行之前引發異常是非常容易的(尤其是在多核處理器上)!修復不應該很難——只需修復調試器,這樣它就可以處理以任意順序顯示這兩個事件。容易的。在這些調試器修復其競爭條件之前,每個命名其線程的應用程序都必須非常小心。

解決它

想出一個解決所有這些問題的辦法是一個有趣的練習。我可以很容易地創建一個導出SetThreadName函數的DLL。然后,這個DLL將與另一個進程通信,該進程將維護一個內存中的數據庫,該數據庫將線程id映射到名稱。為了避免客戶機上的速度減慢,這將是一個選擇加入進程,可能是通過讓程序執行LoadLibrary/GetProcAddress舞蹈來實現的,以查看是否安裝了DLL/進程對。這很簡單,但是這個想法有兩個問題,嚴重到足以讓我不去煩。

我天真的方法無法判斷線程何時死亡,這意味着線程數據庫將迅速增長到數千個條目。這些線程中的大多數都是未使用的,而且許多線程表示重用已命名但現在已死線程的id的線程。添加unsethreadname函數可以減少混亂,但無法解決問題,尤其是在線程突然退出時。在用戶模式下解決這個問題而不引入競爭條件將是一個挑戰。

更大的問題是線程命名API需要廣泛的支持。我可以說服成千上萬的開發人員采用一種新的線程命名API,但是如果沒有工具支持,這將是毫無意義的。我希望這個線程名數據庫由windbg、visualstudio調試器和ETW跟蹤查詢。我沒有這些工具的源代碼,也找不到在哪里創建拉請求。
事實證明,驅動程序可以使用PsSetCreateThreadNotifyRoutine可靠地跟蹤線程的創建和銷毀,從而創建一個線程名稱數據庫。但是,這仍然缺少對工具的支持,所以微軟已經實施了一個官方的解決方案。
在操作系統中添加這種類型的工具是有先例的。gflags工具允許開發人員跟蹤堆分配、對象創建者類型跟蹤等等。現在是微軟添加線程名稱作為gflags選項的時候了。內核可以捕捉現有的異常,可以更新調試器和探查器來查詢內核的線程名稱數據庫,這樣世界會變得更好。
我希望微軟也能修復當前設置中的const正確性,/analyze警告和競爭條件。三個都修好了。

在那之前如果您正在編寫調試器,請考慮添加一個更健壯的線程命名機制。也許會流行起來。您還應該支持現有的標准,並在調試器中修復其競爭條件。如果您正在編寫一個程序,其中您想命名您的線程,那么您所能做的就是稍微改善一下情況。可以用兩種方法之一避免線程命名競爭條件。最簡單的解決方案是讓線程自己命名。這保證線程創建事件在異常事件之前到達,因為子線程在開始運行之前不能調用SetThreadName。如果您想創建線程,然后從creator thread中命名它們,那么您必須等到線程完全啟動。這可以通過等待線程發出一個事件的信號來完成,或者如果這不方便,那么就等“一會兒”然后交叉手指。
您還應該將thread name參數設為constchar*,並使用“#pragma warning(disable:63206322)”來抑制偽/analyze警告

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM