.NET Framework 2.0 中的分析工具堆疊逐步解說:基本概念和更新版本
2006 年 9 月
David Broman
Microsoft Corporation
適用於:
Microsoft .NET Framework 2.0
Common Language Runtime (CLR)
總結:描述如何設計程式碼剖析工具,以在 .NET Framework 的 Common Language Runtime (CLR) 中逐步執行 Managed 堆疊。 (14 個列印頁面)
目錄
簡介
同步和非同步呼叫
混合
請依您的最佳行為
受夠了
點數到期的點數
關於作者
簡介
本文的目標是想要建置分析工具以檢查受控應用程式的任何人為目標。 我將說明如何設計程式碼剖析工具,以在 .NET Framework 的 Common Language Runtime (CLR) 中逐步執行 Managed 堆疊。 我將嘗試讓情緒保持亮起,因為主題本身有時可能會繁重。
CLR 2.0 版中的分析 API 有一個名為 DoStackSnapshot 的新方法,可讓您的分析工具逐步執行您要分析之應用程式的呼叫堆疊。 CLR 1.1 版會透過同進程偵錯介面公開類似的功能。 但是,使用 DoStackSnapshot來逐步執行呼叫堆疊更容易、更精確且更穩定。 DoStackSnapshot方法會使用垃圾收集行程、安全性系統、例外狀況系統等所使用的相同堆疊查核器。 因此 ,您知道 它是正確的。
完整堆疊追蹤的存取權可讓使用者在發生有趣的狀況時,取得應用程式中發生什麼事的全貌。 根據應用程式以及使用者想要分析的內容,您可以想像使用者在設定物件時想要呼叫堆疊、載入類別時、擲回例外狀況時等等。 即使取得應用程式事件以外的專案呼叫堆疊,例如計時器事件,對取樣分析工具會感興趣。 當您可以看到呼叫函式的函式呼叫包含作用點之函式的人員時,查看程式碼中的作用點會更加啟發。
我將著重于使用 DoStackSnapshot API 取得堆疊追蹤。 取得堆疊追蹤的另一種方式是建置陰影堆疊:您可以攔截 FunctionEnter 和 FunctionLeave ,以保留目前線程的 Managed 呼叫堆疊複本。 如果您在應用程式執行期間隨時需要堆疊資訊,而且不考慮讓分析工具的程式碼在每個 Managed 呼叫上執行並傳回的效能成本,陰影堆疊建置就很有用。 如果您需要稍微疏散的堆疊報告,例如回應事件, DoStackSnapshot 方法是最佳方法。 即使是每隔幾毫秒擷取堆疊快照集的取樣分析工具,還是比建置陰影堆疊更疏疏。 因此 DoStackSnapshot 非常適合取樣分析工具。
在通配端採取堆疊逐步解說
每當您想要堆疊時,就能取得呼叫堆疊非常有用。 但有了能力,就負責了。 分析工具使用者不希望堆疊逐步執行會導致存取違規 (AV) 或執行時間中的死結。 身為分析工具寫入器,您必須小心使用您的電源。 我將討論如何使用 DoStackSnapshot,以及如何謹慎執行這項操作。 如您所見,您想要使用此方法執行的動作越多,就越難正確。
讓我們看看我們的主旨。 以下是分析工具呼叫的內容, (您可以在 Corprof.idl) 的 ICorProfilerInfo2 介面中找到:
HRESULT DoStackSnapshot(
[in] ThreadID thread,
[in] StackSnapshotCallback *callback,
[in] ULONG32 infoFlags,
[in] void *clientData,
[in, size_is(contextSize), length_is(contextSize)] BYTE context[],
[in] ULONG32 contextSize);
下列程式碼是您分析工具上的 CLR 呼叫的內容。 (您也可以在 Corprof.idl.) 您在上述範例的 callback 參數中傳遞此函式實作的指標。
typedef HRESULT __stdcall StackSnapshotCallback(
FunctionID funcId,
UINT_PTR ip,
COR_PRF_FRAME_INFO frameInfo,
ULONG32 contextSize,
BYTE context[],
void *clientData);
就像三明治一樣。 當您的分析工具想要逐步執行堆疊時,您會呼叫 DoStackSnapshot。 CLR 從該呼叫傳回之前,它會多次呼叫 StackSnapshotCallback 函式,一次用於每個 Managed 框架,或針對堆疊上每個 Unmanaged 框架執行一次。 圖 1 顯示這個三明治。
圖 1. 分析期間呼叫的「三明治」
如您在我的標記法中看到,CLR 會以反向順序通知您畫面格如何推送到堆疊—分葉框架第一個 (推送最後一個) ,主要畫面格最後 (先推送) 。
這些函式的所有參數都代表什麼意思? 我尚未準備好全部討論,但從 DoStackSnapshot開始,我將會討論其中一些。 (我稍後會進入其餘部分。) infoFlags 值來自 Corprof.idl 中的 COR_PRF_SNAPSHOT_INFO 列舉,並可讓您控制 CLR 是否為您註冊所報告框架的內容。 您可以為 clientData 指定任何想要的值,CLR 會在 StackSnapshotCallback 呼叫中將它傳回給您。
在 StackSnapshotCallback中,CLR 會使用 funcId 參數來傳遞目前逐步解說框架的 FunctionID 值。 如果目前的框架是 Unmanaged 框架的執行,則此值為 0,稍後將討論。 如果 funcId 為非零值,您可以將 funcId 和 frameInfo 傳遞給其他方法,例如 GetFunctionInfo2 和 GetCodeInfo2,以取得函式的詳細資訊。 您可以在堆疊逐步執行期間立即取得此函式資訊,或儲存 funcId 值,稍後再取得函式資訊,以減少您對執行中應用程式的影響。 如果您稍後收到函式資訊,請記住 frameInfo 值只在回呼內提供它給您。 雖然可以儲存 funcId 值以供稍後使用,但請勿儲存 frameInfo 以供稍後使用。
當您從 StackSnapshotCallback傳回時,通常會傳回 S_OK ,而 CLR 會繼續執行堆疊。 如有需要,您可以傳回 S_FALSE,這會停止堆疊逐步解說。 您的 DoStackSnapshot 呼叫會接著傳回 CORPROF_E_STACKSNAPSHOT_ABORTED。
同步和非同步呼叫
您可以透過兩種方式以同步和非同步方式呼叫 DoStackSnapshot 。 同步呼叫是最簡單的方法。 當 CLR 呼叫其中一個分析工具的 ICorProfilerCallback (2) 方法時,您會進行同步呼叫,並在回應中呼叫 DoStackSnapshot 以逐步執行目前線程的堆疊。 當您想要查看堆疊在 物件Allocated之類的有趣通知點上的外觀時,這會很有用。 若要執行同步呼叫,您可以從ICorProfilerCallback (2) 方法內呼叫DoStackSnapshot,針對我尚未告訴您的參數傳遞零或Null。
當您逐步執行不同執行緒的堆疊,或強制中斷線程執行堆疊逐步解說時,就會發生非同步堆疊逐步解說, (本身或在另一個執行緒上執行) 。 中斷線程牽涉到攔截執行緒的指令指標,以強制它任意時間執行您自己的程式碼。 基於太多原因而無法在此列出,這很危險。 請不要這麼做。 我將會限制非同步堆疊逐步解說的描述,以使用 DoStackSnapshot 來逐步解說個別的目標執行緒。 我呼叫這個「非同步」,因為目標執行緒是在堆疊逐步解說開始的任意時間點執行。 取樣分析工具通常會使用這項技術。
逐一查看其他人
讓我們細分跨執行緒,也就是非同步堆疊逐步解說一點。 您有兩個執行緒:目前的執行緒和目標執行緒。 目前的執行緒是執行 DoStackSnapshot 的執行緒。 目標執行緒是 DoStackSnapshot正在逐步執行其堆疊的執行緒。 您可以將執行緒參數中的執行緒識別碼傳遞至DoStackSnapshot,以指定目標執行緒。 接下來會發生什麼事,不是心心模糊。 請記住,當您要求逐步執行其堆疊時,目標執行緒正在執行任意程式碼。 因此,CLR 會暫停目標執行緒,而且會隨時暫停它正在逐步執行。 這可以安全地完成嗎?
很高興您問。 這確實很危險,我稍後會討論如何安全地執行這項操作。 但首先,我要進入混合模式堆疊。
混合
受控應用程式不一定會在 Managed 程式碼中花費其所有時間。 PInvoke 呼叫和 COM Interop 可讓 Managed 程式碼呼叫 Unmanaged 程式碼,有時會再次使用委派返回。 而 Managed 程式碼會直接呼叫 Unmanaged 執行時間, (CLR) 來執行 JIT 編譯、處理例外狀況、執行垃圾收集等等。 因此,當您執行堆疊逐步解說時,可能會遇到混合模式堆疊—有些畫面格是 Managed 函式,而其他則為 Unmanaged 函式。
成長,已經!
在繼續之前,先進行簡短的插播。 每個人都知道新式電腦上的堆疊會成長 (,也就是「推送」) 到較小的位址。 但是,當我們在心中或白板上視覺化這些位址時,我們會不小心如何垂直排序這些位址。 我們有些假設堆疊 在 頂端) (小位址成長;有些使用者看到它逐漸 減少 , (底部) 的位址。 我們也會在小組中分割此問題。 我選擇與我曾使用過的任何偵錯工具側邊,呼叫堆疊追蹤和記憶體傾印會告知我小型位址是「上方」大位址。 因此堆疊會成長;main 位於底部,分葉被呼叫者位於頂端。 如果您不一致,您必須進行一些心意重新排列,才能完成本文的這一部分。
等候者,我的堆疊中有漏洞
既然我們要說出相同的語言,讓我們看看混合模式堆疊。 圖 2 說明混合模式堆疊的範例。
圖 2. 具有 Managed 和 Unmanaged 框架的堆疊
回顧一下,請務必瞭解 DoStackSnapshot 第一次存在的原因。 其可協助您逐步解說堆疊上的 受控 框架。 如果您嘗試自行逐步解說 Managed 框架,您會得到不可靠的結果,特別是在 32 位系統上,因為 Managed 程式碼中使用的某些 Wacky 呼叫慣例。 CLR 瞭解這些呼叫慣例, DoStackSnapshot 因此可協助您解碼這些慣例。 不過,如果您想要能夠逐步執行整個堆疊,包括 Unmanaged 框架, DoStackSnapshot 不是完整的解決方案。
以下是您選擇的位置:
選項 1:不對使用者執行任何動作,並以「非受控漏洞」回報堆疊,或 ...
選項 2:撰寫您自己的 Unmanaged 堆疊逐步解說工具,以填入這些漏洞。
當 DoStackSnapshot跨越非受控框架區塊時,它會呼叫StackSnapshotCallback函式,並將funcId設定為
0,如前所述。 如果您使用選項 1,只要 在 funcId 為 0 時在回呼中執行任何動作即可。 CLR 會針對下一個受管理框架再次呼叫您,而且您可以在該時間點喚醒。
如果 Unmanaged 區塊包含一個以上的非受控框架,CLR 仍只會呼叫 StackSnapshotCallback 一次。 請記住,CLR 不費力地解碼 Unmanaged 區塊—其具有特殊的測試人員資訊,可協助略過區塊到下一個受管理框架,這就是其進度。 CLR 不一定知道 Unmanaged 區塊內的內容。 這可讓您瞭解,因此選項 2。
第一個步驟是 Doozy
無論您選擇哪一個選項,填入 Unmanaged 漏洞並不是唯一的硬性部分。 只是開始逐步解說可能是一項挑戰。 請查看上方的堆疊。 頂端有 Unmanaged 程式碼。 有時候您會很快樂,而 Unmanaged 程式碼將是 COM 或 PInvoke 程式碼。 如果是這樣,CLR 就足以知道如何略過它,並開始在範例中的第一個 Managed 框架 (D 的逐步解說) 。 不過,您可能仍想要逐步執行最上層 Unmanaged 區塊,以盡可能報告完成堆疊。
即使您不想要逐步執行最上層的區塊,您還是可能會被強制執行,如果您 不 快樂,該 Unmanaged 程式碼不是 COM 或 PInvoke 程式碼,而是 CLR 本身的協助程式程式碼,例如執行 JIT 編譯或垃圾收集的程式碼。 如果是這種情況,CLR 將無法在沒有協助的情況下找到 D 框架。 因此, DoStackSnapshot 的未實作呼叫會導致錯誤 CORPROF_E_STACKSNAPSHOT_UNMANAGED_CTX 或 CORPROF_E_STACKSNAPSHOT_UNSAFE。 (如此,請造訪 corerror.h.)
請注意,我使用了「unseeded」 這個字。DoStackSnapshot會使用內容和coNtextSize
參數來取得種子內容。 「coNtext」 這個字會多載許多意義。 在此情況下,我正在討論註冊內容。 例如,如果您將架構相依的 Windows 標頭 (,nti386.h) 您會找到名為 CONTEXT的結構。 它包含 CPU 暫存器的值,並代表 CPU 在特定時間點的狀態。 這就是我所討論的內容類型。
如果您傳遞內容參數的Null,堆疊逐步解說會取消實作,而 CLR 會從頂端開始。 不過,如果您為 內容 參數傳遞非 Null 值,表示堆疊上某個位置下方的 CPU 狀態 (,例如指向 D 框架) ,CLR 會執行植入您內容的堆疊逐步解說。 它會忽略堆疊的實際頂端,並在您指向的位置開始。
沒關係,不是真的。 您傳遞至 DoStackSnapshot 的內容比直接指示詞還多。 如果 CLR 確定它可以找到第一個 Managed 框架 (,因為最上層的 Unmanaged 區塊是 PInvoke 或 COM 程式碼) ,它會這麼做並忽略您的種子。 不過,請勿以個人方式加以採用。 CLR 正嘗試藉由提供最精確的堆疊逐步解說來協助您。 只有在最上層 Unmanaged 區塊是 CLR 本身的協助程式程式碼時,您的種子才有用,因為我們沒有資訊可協助我們略過它。 因此,只有在 CLR 無法自行判斷開始逐步解說的位置時,才會使用您的種子。
您可能會想知道如何在第一個地方提供種子給我們。 如果目標執行緒尚未暫停,您就無法只逐步執行目標執行緒的堆疊來尋找 D 框架,進而計算您的種子內容。 不過,我在呼叫DoStackSnapshot之前先執行 Unmanaged 逐步解說來計算種子內容,因此在 DoStackSnapshot為您暫停目標執行緒之前。 目標執行緒是否需要由您 和 CLR 暫停? 實際上,是。
我認為這是一個準備的時機。 但在我太深之前,請注意,是否以及如何植入堆疊逐步解說的問題僅適用于 非同步 逐步解說。 如果您正在執行同步逐步解說, DoStackSnapshot 一律能夠找到最上層受控框架的方式,而不需要任何種子。
全部一起
對於正在執行非同步、跨執行緒、植入堆疊逐步解說,同時填入 Unmanaged 漏洞時,真正的 adventuresome 分析工具,以下是堆疊逐步解說的外觀。 假設此處所說明的堆疊與您在圖 2 中看到的堆疊相同,只是分了一點。
堆疊內容 | Profiler 和 CLR 動作 |
---|---|
1. 您暫停目標執行緒。 (目標執行緒的暫停計數現在是 1.) 2.您會取得目標執行緒目前的暫存器內容。 3.您可以判斷暫存器內容是否指向 Unmanaged 程式碼,也就是呼叫 ICorProfilerInfo2::GetFunctionFromIP ,並檢查您是否取得 0 的 FunctionID 值。 4.因為在此範例中,暫存器內容確實指向 Unmanaged 程式碼,所以您會執行 Unmanaged 堆疊逐步解說,直到您找到最上層受控框架 (函式 D) 為止。 |
|
5.您可以使用種子內容呼叫 DoStackSnapshot ,而 CLR 會再次暫停目標執行緒。 (其暫停計數現在是 2.) 三明治開始。
a. CLR 會使用 D 的FunctionID呼叫StackSnapshotCallback函式。 |
|
b. CLR 會呼叫 您的 StackSnapshotCallback 函式,其 FunctionID 等於 0。 您必須自行執行此區塊。 當您到達第一個受管理框架時,可以停止。 或者,您可以速查並延遲 Unmanaged 逐步執行,直到下一次回呼之後,因為下一個回呼會告訴您下一個 Managed 框架的開始位置,因此 Unmanaged 逐步解說應該結束的位置。 |
|
c. CLR 會使用 C 的 FunctionID 呼叫 StackSnapshotCallback 函 式 。 |
|
d. CLR 會使用 B 的 FunctionID 呼叫 StackSnapshotCallback 函 式 。 |
|
e. CLR 會呼叫 您的 StackSnapshotCallback 函式,其 FunctionID 等於 0。 同樣地,您必須自行執行此區塊。 |
|
f. CLR 會使用 A 的 FunctionID 呼叫 StackSnapshotCallback 函 式 。 |
|
g. CLR 會使用 Main 的FunctionID呼叫StackSnapshotCallback函式。 |
|
6.繼續目標執行緒。 其暫停計數現在是 0,因此執行緒實際上會繼續。 |
請依您的最佳行為
沒關係,這是太多能力,而不需特別小心。 在最進階的案例中,您會回應計時器中斷並任意暫停應用程式執行緒,以逐步執行其堆疊。 Yikes!
良好很困難,而且牽涉到一開始不明顯的規則。 因此,讓我們深入探討。
不正確的種子
讓我們從簡單的規則開始:不要使用不正確的種子。 如果您在呼叫 DoStackSnapshot時,分析工具提供無效 (非 Null) 種子,CLR 會提供不正確的結果。 它會查看指向它的堆疊,並假設堆疊上的值應該代表什麼。 這會導致 CLR 取值假設為堆疊上的位址。 假設有不正確的種子,CLR 會將值從記憶體中的某個未知位置取值。 CLR 會執行一切作業,以避免全機的第二次 AV,這會終止您正在分析的程式。 但您真的應該努力讓種子正確。
擱置
暫停執行緒的其他層面十分複雜,足以要求多個規則。 當您決定執行跨執行緒逐步執行時,您至少決定要求 CLR 代表您暫停執行緒。 此外,如果您想要在堆疊頂端逐步執行 Unmanaged 區塊,您決定自行暫停執行緒,而不需叫用 CLR 目前是否為好主意。
如果您採用電腦科學課程,您可能會記住「原理」問題。 一組原理正在一個資料表上,各有一個分叉在右邊和左邊一個分支。 根據問題,它們都需要兩個分叉來吃。 每個原理都會挑選他右邊的分叉,但沒有人可以挑選左派生,因為每個原理都在等候左方原理放下所需的分叉。 而且,如果原理位在圓形資料表上,您就會有一個迴圈等候,而且有許多空的鐺子。 它們全都耗盡的原因是它們會中斷簡單的死結避免規則:如果您需要多個鎖定,請一律以相同的連續處理。 遵循此規則可避免 A 在 B、B 等候 C 和 C 等候 A 的迴圈。
假設應用程式遵循規則,且一律會以相同順序接受鎖定。 現在元件會隨著程式碼剖析工具 (,例如) 並開始任意暫停執行緒。 複雜度已大幅增加。 如果暫停者現在需要擷取被暫停者持有的鎖定,該怎麼辦? 或者,如果暫停者需要等候另一個正在等候暫停者所持有鎖定的執行緒所持有的鎖定,該怎麼辦? 暫停會將新的邊緣新增至執行緒相依性圖形,這可能會導致迴圈。 讓我們看看一些特定問題。
問題 1:暫停者擁有暫停者所需的鎖定,或暫停者相依的執行緒所需的鎖定。
問題 1a:鎖定是 CLR 鎖定。
如您所想像,CLR 會執行許多執行緒同步處理,因此有數個鎖定在內部使用。 當您呼叫 DoStackSnapshot時,CLR 會偵測到目標執行緒擁有 CLR 鎖定,而目前的執行緒 (呼叫 DoStackSnapshot 的執行緒) 才能執行堆疊逐步解說。 當發生該狀況時,CLR 會拒絕執行暫停,而 DoStackSnapshot 會立即傳回錯誤 CORPROF_E_STACKSNAPSHOT_UNSAFE。 此時,如果您在呼叫 DoStackSnapshot之前自行暫停執行緒,則您將自行繼續執行緒,並避免發生問題。
問題 1b:鎖定是您自己的分析工具鎖定。
這個問題真的是常見的問題。 您可能有自己的執行緒同步處理,可以在這裡和該處執行。 假設應用程式執行緒 (執行緒 A) 遇到分析工具回呼,並執行一些流量分析工具鎖定的分析工具程式碼。 然後執行緒 B 必須逐步執行執行緒 A,這表示執行緒 B 會暫停執行緒 A。您必須記住,當執行緒 A 暫停時,您不應該嘗試讓執行緒 B 嘗試取得執行緒 A 可能擁有的任何分析工具本身鎖定。 例如,執行緒 B 會在堆疊逐步解說期間執行 StackSnapshotCallback ,因此線上程 A 可能擁有的回呼期間,您不應該採取任何鎖定。
問題 2:當您暫停目標執行緒時,目標執行緒會嘗試暫停您。
您可能會說:「無法發生!」如果:
- 您的應用程式會在多處理器方塊上執行,而且
- 執行緒 A 會在一個處理器上執行,而執行緒 B 在另一個處理器上執行,而且
- 執行緒 A 嘗試線上程 B 嘗試暫停執行緒 A 時暫停執行緒 B。
在此情況下,兩個暫停都可能勝出,而兩個執行緒最終都會暫停。 因為每個執行緒正在等候另一個執行緒喚醒,所以會永遠暫停。
此問題比問題 1 更不連續,因為您無法依賴 CLR 來偵測, 再 呼叫 DoStackSnapshot ,讓執行緒彼此暫停。 執行暫停之後,太晚了!
為什麼目標執行緒嘗試暫停分析工具? 在假設性、撰寫不佳的分析工具中,堆疊逐步解說程式碼以及暫停程式碼可能會在任意時間由任意數目的執行緒執行。 假設執行緒 A 嘗試線上程 B 嘗試逐步執行執行緒 A 時,正在嘗試執行執行緒 B。兩者都嘗試同時暫停彼此,因為它們都是執行分析工具堆疊逐步執行常式的 SuspendThread 部分。 win 和正在分析的應用程式都會死結。 這裡的規則很明顯,不允許分析工具執行堆疊式程式碼 (,因此暫停程式碼會在兩個執行緒上同時) !
目標執行緒可能會因為 CLR 的內部運作而嘗試暫停您的逐步執行執行緒的較不明顯原因。 CLR 會暫停應用程式執行緒,以協助處理垃圾收集等工作。 如果您的逐步解說程式嘗試 (,因此暫停) 執行垃圾收集的執行緒,同時垃圾收集行程執行緒嘗試暫停您的查核程式,進程將會死結。
但很容易避免問題。 CLR 只會暫停它必須暫停的執行緒,才能執行其工作。 假設您的堆疊逐步解說涉及兩個執行緒。 執行緒 W 是執行逐步解說) 執行緒 (目前的執行緒。 執行緒 T 是 (堆疊) 所逐步執行之執行緒的目標執行緒。 只要 Thread W 從未執行 Managed 程式碼,因此不受 CLR 垃圾收集限制,CLR 永遠不會嘗試暫停 Thread W。這表示分析工具可以放心地讓執行緒 W 暫止執行緒 T。
如果您要撰寫取樣分析工具,請務必確定這一切。 您通常會有個別的建立執行緒,以回應計時器中斷,並逐步執行其他執行緒的堆疊。 呼叫您的取樣器執行緒。 由於您自行建立取樣器執行緒,並控制其執行 (,因此永遠不會執行 Managed 程式碼) ,因此 CLR 不會有理由暫停它。 設計程式碼剖析工具,使其建立自己的取樣執行緒來執行所有堆疊逐步解說,也會避免先前所述的「撰寫不佳的分析工具」問題。 取樣器執行緒是您嘗試逐步執行或暫停其他執行緒之分析工具的唯一線程,因此分析工具永遠不會嘗試直接暫停取樣器執行緒。
這是我們的第一個非Trivial 規則,因此為了強調,讓我們重複它:
規則 1:只有從未執行 Managed 程式碼的執行緒應該暫停另一個執行緒。
沒有人喜歡逐步解說
如果您正在執行跨執行緒堆疊逐步解說,您必須確保目標執行緒在逐步解說期間保持運作。 只是因為您將目標執行緒當做參數傳遞至 DoStackSnapshot 呼叫,並不表示您已隱含新增任何類型的存留期參考。 應用程式可以隨時讓執行緒消失。 如果您在嘗試逐步執行執行緒時發生這種情況,您可以輕鬆地造成存取違規。
幸運的是,CLR 會線上程即將終結時通知分析工具,使用以ICorProfilerCallback (2) 介面定義的 aptly 命名的 ThreadDestroyed回呼。 您必須負責實作 ThreadDestroyed ,並讓它等到執行緒執行完成的任何進程為止。 這很有趣,足以符合我們的下一個規則:
規則 2:覆寫 ThreadDestroyed 回呼,並讓實作等到您完成執行緒堆疊終結為止。
遵循規則 2 會封鎖 CLR 終結執行緒,直到您完成該執行緒堆疊的逐步執行為止。
垃圾收集可協助您建立迴圈
此時,事情可能會有點令人困惑。 讓我們從下一個規則的文字開始,並從該處解碼:
規則 3:在可觸發垃圾收集的分析工具呼叫期間,請勿保留鎖定。
我稍早提到,如果您的分析工具在擁有線程可能暫停時有自己的鎖定,以及執行緒可能由另一個需要相同鎖定的執行緒逐步執行,則為錯誤的主意。 規則 3 可協助您避免更細微的問題。 在此,如果擁有的執行緒即將呼叫ICorProfilerInfo (2) 方法,則不應該保留任何自己的鎖定,而該方法可能會觸發垃圾收集。
幾個範例應該有所説明。 在第一個範例中,假設 Thread B 正在執行垃圾收集。 序列為:
- 執行緒 A 會接受並擁有其中一個分析工具鎖定。
- 執行緒 B 會呼叫分析工具的 GarbageCollectionStarted 回 呼。
- 步驟 1 之分析工具鎖定上的執行緒 B 區塊。
- 執行緒 A 會執行 GetClassFromTokenAndTypeArgs 函式。
- GetClassFromTokenAndTypeArgs呼叫會嘗試觸發垃圾收集,但會偵測到垃圾收集正在進行中。
- 執行緒 A 區塊,正在等候目前進行中的垃圾收集, (執行緒 B) 完成。 不過,由於分析工具鎖定,執行緒 B 正在等候執行緒 A。
圖 3 說明此範例中的案例:
圖 3. 分析工具與垃圾收集行程之間的死結
第二個範例是稍微不同的案例。 序列為:
- 執行緒 A 會接受並擁有其中一個分析工具鎖定。
- 執行緒 B 會呼叫分析工具的 ModuleLoadStarted 回 呼。
- 步驟 1 之分析工具鎖定上的執行緒 B 區塊。
- 執行緒 A 會執行 GetClassFromTokenAndTypeArgs 函式。
- GetClassFromTokenAndTypeArgs呼叫會觸發垃圾收集。
- 執行緒 A (,目前正在執行垃圾收集) 等候執行緒 B 準備好收集。 但執行緒 B 正在等候執行緒 A,因為您的分析工具鎖定。
- 圖 4 說明第二個範例。
圖 4. 分析工具與擱置垃圾收集之間的死結
您是否已摘要這些不想要? 問題的關鍵在於垃圾收集有自己的同步處理機制。 因為一次只能發生一個垃圾收集,所以會發生第一個範例的結果。 這基本上是一個邊緣案例,因為垃圾收集通常不會經常發生,除非您在壓力情況下運作,否則必須等候另一個。 即使如此,如果您分析的時間夠長,就會發生此案例,而且您必須為其做好準備。
發生第二個範例的結果是因為執行垃圾收集的執行緒必須等候其他應用程式執行緒準備好進行收集。 當您將其中一個自己的鎖定引入混合中,因而形成迴圈時,就會發生此問題。 在這兩種情況下,規則 3 都會中斷,方法是讓執行緒 A 擁有其中一個分析工具鎖定,然後呼叫 GetClassFromTokenAndTypeArgs。 (實際上,呼叫任何可能會觸發垃圾收集的方法就足以使 process.)
您可能現在有數個問題。
Q. 如何知道哪一個 ICorProfilerInfo (2) 方法可能會觸發垃圾收集?
A. 我們打算在 MSDN 上記載此檔,或至少在 部落格 或 Jonathan Keljo 的部落格中記載。
Q. 這與堆疊逐步執行有何關聯? 沒有 DoStackSnapshot 的提及。
A. True。 而且 DoStackSnapshot 甚至不是其中一個 ICorProfilerInfo (2 個觸發 垃圾收集的) 方法。 我在這裡討論規則 3 的原因是,這些有風險的程式設計人員會以非同步方式從可能實作自己的分析工具鎖定的任意範例中逐步執行堆疊,因此很容易落入這個陷阱。 事實上,規則 2 基本上會告訴您將同步處理新增至分析工具。 取樣分析工具也可能有其他同步處理機制,或許可以協調讀取和寫入任意時間的共用資料結構。 當然,永遠不會觸碰 DoStackSnapshot 的分析工具仍可能遇到此問題。
受夠了
我即將完成重點摘要。 以下是要記住的重要重點:
- 同步堆疊逐步解說涉及逐步解說目前的執行緒,以回應分析工具回呼。 這些不需要植入、暫停或任何特殊規則。
- 如果堆疊頂端是 Unmanaged 程式碼,而不是 PInvoke 或 COM 呼叫的一部分,則非同步逐步解說需要種子。 您可以直接暫停目標執行緒並自行執行,直到您找到最上層的受控框架,以提供種子。 如果您在此案例中未提供種子, DoStackSnapshot 可能會傳回失敗碼,或略過堆疊頂端的某些畫面格。
- 如果您需要暫停執行緒,請記住只有從未執行 Managed 程式碼的執行緒應該暫停另一個執行緒。
- 執行非同步逐步解說時,請一律覆寫 ThreadDestroyed 回 呼,以封鎖 CLR 終結執行緒,直到該執行緒的堆疊逐步解說完成為止。
- 當分析工具呼叫可觸發記憶體回收的 CLR 函式時,請勿保留鎖定。
如需分析 API 的詳細資訊,請參閱 MSDN 網站上的 分析 (非受控) 。
點數到期的點數
我想要包含一份注意事項,感謝 CLR 分析 API 小組的其餘部分,因為撰寫這些規則確實是小組工作。 特別感謝 Sean Selitrennikoff,他提供許多先前的內容。
關於作者
David 是 Microsoft 的開發人員,超過您的想法,因為他有有限的知識與成熟度。 雖然不再允許簽入程式碼,但仍提供新變數名稱的想法。 David 是 Count Chocula 的熱力風扇,擁有自己的汽車。