本文章是由機器翻譯。

非同步程式設計

非同步事故鏈追蹤

Andrew Stasyuk

下載代碼示例

 

與 C# 5 的到來,Visual Basic.NET 11,Microsoft.NET Framework 4.5 和 Windows 存儲區為.NET 應用程式、 非同步程式設計經驗已被精簡極大地。 新的非同步等待關鍵字 (非同步和等待中Visual Basic) 允許開發人員保持它們被用來編寫同步代碼時的同一抽象。

大量的精力是放入Visual Studio2012 改善非同步並行堆疊、 並行任務、 並行手錶和併發視覺化檢視等工具調試。 然而,正在與調試經驗的同步代碼,我們在不很有尚未。

打破了抽象和揭示背後等待非同步/大三巴牌坊內部水管更突出的問題之一是缺乏在調試器中呼叫堆疊資訊。 在本文中,我提供手段,彌合這種差距和改善您的.NET 4.5 或 Windows 存儲應用程式非同步調試體驗。

讓我們首先解決基本的術語。

呼叫堆疊的定義

MSDN 文檔 (bit.ly/Tukvkm) 用來定義呼叫堆疊作為"從程式的開頭通往目前在運行時執行的語句的方法調用的系列"。這種觀念是完全有效的單線程、 同步程式設計模型,但現在,並行性和非同步獲得勢頭,更精確的分類是必要的。

本文中,是重要的是要區分從返回堆疊的因果關係鏈。 在同步模式中,這兩個名詞大部分都相同 (稍後我會提及例外情況下)。 在非同步代碼中,上述定義描述的因果關係鏈。

另一方面,當前正在執行,當完成後,該語句將導致一系列繼續其執行的方法。 這一系列構成返回堆疊。 或者,為讀者熟悉繼續傳遞樣式 (EricLippert 有很棒的一系列關於這一主題,起價 bit.ly/d9V0Dc),返回堆疊可能定義為一系列延續註冊要執行,應當前執行的方法完成。

簡而言之,因果關係鏈回答,"我怎麼在這裡?"返回堆疊時的答案,"我到哪裡下?"例如,如果你有一個鎖死在您的應用程式,您可能能夠找出是什麼導致它從前者,而後者會讓你知道後果是什麼。 請注意雖然因果關係鏈始終跟蹤回程序進入點,返回堆疊切斷在非同步作業的結果都沒有遵守 (為例如,非同步 void 方法或通過 ThreadPool.QueueUserWorkItem 計畫工時) 點。

此外,還有一個被保留用於診斷 ; 同步呼叫堆疊的副本的堆疊追蹤的概念 我將交替使用這兩個名詞。

請注意有幾個心照不宣的假設,在前面的定義:

  • "方法調用"所指的第一定義中一般暗示"的方法,仍未完成,"而承受同步程式設計模型中的物理意義的"是在堆疊上"。 然而,雖然我們通常不感興趣已經返回的方法,它總是不可能將它們區分在非同步調試過程中。 在這種情況下,沒有物理概念的"堆疊上的是"和所有延續都相等的因果關係鏈的有效元素。
  • 即使在同步代碼中,因果關係鏈和返回堆疊不總是相同。 一個特別的案件時一種方法可能會出現在一個,但從其他,失蹤是尾調用。 雖然講得不直接通在 C# 和Visual Basic.NET 可能編碼在中間語言 (IL) ("尾巴"。 首碼) 或者 (尤其是在 64 位進程) 中時 (JIT) 編譯器生成的。
  • 最後但並非最不重要,因果關係鏈和返回堆疊可以是非線性。 他們就是最一般的情況下,他們只是圖有接收器 (因果圖) 或源 (返回圖) 作為當前語句。 在非同步代碼中的非線性是由於叉 (源自一個並行非同步作業) 和聯接 (繼續計畫運行一組並行非同步作業完成時)。 本文中,並且由於平臺限制 (稍後解釋),我認為只有線性因果關係鏈和返回堆疊,哪些相應的圖的子集。

幸運的是,如果不同步引入一個程式通過使用非同步和等待關鍵字與沒有叉或聯接,並且正在等待所有非同步方法,因果關係鏈仍然相同是返回堆疊,同步代碼中一樣。 在這種情況下,他們倆都是同樣有益於你控制流中的定位。

另一方面,因果關係鏈相等很少返回堆疊在程式中顯式地雇用計畫延續,一個顯著的例子是任務並行庫 (TPL) 資料流程。 這是由於資料從源塊流到目標塊,永遠不會返回到前者的性質。

現有的工具

請考慮一個簡單的例子:

static void Main()
{
  OperationAsync().Wait();
}
async static Task OperationAsync()
{
  await Task.Delay(1000);
  Console.WriteLine("Where is my call stack?");
}

由外推的開發者已經習慣了在同步調試中的抽象,他們希望看到下面的因果關係鏈/返回堆疊執行暫停在 Console.WriteLine 方法時:

ConsoleSample.exe!ConsoleSample.Program.OperationAsync() Line 19
ConsoleSample.exe!ConsoleSample.Program.Main() Line 13

但是,如果您嘗試這樣做,你就會發現在呼叫堆疊視窗中的 Main 方法中丟失,直接在前面加 [恢復非同步方法] 的 OperationAsync 方法中的堆疊追蹤啟動時。 並行堆疊有兩種方法 ; 然而,它不會顯示主調用 OperationAsync。 並行任務並不能説明,顯示"沒有任務以顯示"。

附註:此時,調試器是意識到 Main 方法的呼叫堆疊的一部分 — — 你可能已經注意到,通過對 OperationAsync 的調用背後的灰色背景。 CLR 和 Windows 運行庫 (WinRT) 有知道在哪裡,繼續執行後返回最頂層的堆疊幀 ; 因此,它們實際上存儲返回堆疊。 在這篇文章,不過,我將只深入跟蹤,作為另一篇文章的主題離開返回堆疊的因果關係。

保留的因果關係鏈

事實上,因果關係鏈永遠不會存儲由運行庫中。 即使您看到調試時同步代碼是從本質上講的呼叫堆疊返回堆疊 — — 正如剛才所說的話,他們是 CLR 和 Windows 運行時必須知道哪些方法執行後返回最頂層的框架。 運行時不需要知道是什麼導致了要執行的特定方法。

若要能夠查看期間活的因果關係鏈和驗屍調試,必須明確保留他們一路走來。 據推測,這將需要存儲位置安排延續每一點 (同步) 的堆疊追蹤資訊和將此資料還原時繼續開始執行。 這些堆疊的跟蹤段可以然後縫合在一起形成的因果關係鏈。

我們更感興趣的因果關係資訊傳輸跨越等待構造,因為這是抽象的相似性與同步代碼出現失靈。 讓我們看看如何以及何時可以捕獲此資料。

正如StephenToub 指出 (bit.ly/yF8eGu),但條件是 FooAsync 返回一項任務,下面的代碼:

await FooAsync();
RestOfMethod();

這大致相當於編譯器進行轉換:

var t = FooAsync();
var currentContext = SynchronizationContext.Current;
t.ContinueWith(delegate
{
  if (currentContext == null)
    RestOfMethod();
  else
    currentContext.Post(delegate { RestOfMethod(); }, null);
}, TaskScheduler.Current);

從擴大的代碼來看,似乎有至少兩個擴充點,可能會允許捕獲因果關係資訊:TaskScheduler 和 SynchronizationCoNtext。 事實上,兩者都提供類似成對的虛擬方法,它應該有可能在正確的時刻捕獲呼叫堆疊段:QueueTask/TryDequeue TaskScheduler 和郵政/OperationStarted SynchronizationCoNtext 上。

不幸的是,顯式等等計畫通過協力廠商物流的 API,如 Task.Run、 Task.ContinueWith、 TaskFactory.StartNew 的委託時,只可以替代預設 TaskScheduler。 這意味著每當外面正在運行的任務安排延續,預設的 TaskScheduler 將生效。 因此,TaskScheduler-­的的做法將無法獲取必要的資訊。

至於 SynchronizationCoNtext,雖然它是可以重寫預設實例的當前執行緒的此類通過調用 SynchronizationCoNtext.SetSynchronizationCoNtext 方法,這已在應用程式中的每個執行緒的工作要做。 因此,必須要能夠控制執行緒存留期,這是不可行的如果你不打算重新實現執行緒池。 此外,Windows 表單、Windows Presentation Foundation(WPF) 和ASP.NET提供的 SynchronizationCoNtext SynchronizationCoNtext.Default,其中安排到執行緒池工作除了自己的實現。 因此,您的實現必須根據起源的執行緒,它正在以不同的方式表現。

此外請注意,當等待 awaitable 自訂時,它是完全由執行是否能夠使用 SynchronizationCoNtext 來計畫繼續進行。

幸運的是,有兩個擴充點適合我們的場景:訂閱協力廠商物流事件而無需修改現有的基本代碼,或明確選取通過略有修改每個等待應用程式中的運算式。 第一種方法僅適用于.NET 的桌面應用程式,而第二個可以容納 Windows 存儲區的應用程式。 我會詳細說明在以下各節中的兩個。

介紹 EventSource

.NET Framework 支援事件跟蹤 Windows (ETW),定義了幾乎每個方面的運行時事件提供程式 (bit.ly/VDfrtP)。 特別是,協力廠商物流將觸發事件使您可以跟蹤任務的存留期。 雖然不是所有這些事件都有記錄,您可以獲取它們的定義自己的深入研究與工具 (如 ILSpy 或反射器 mscorlib.dll 或偷窺框架參考源 (bit.ly/HRU3) 和搜索的 TplEtwProvider 類。 當然,通常反射免責聲明適用:API 不記錄,則不能保證將在下一版本中保留根據經驗觀察到的行為。

TplEtwProvider 從 System.Diagnostics.Tracing.EventSource,這在.NET 框架 4.5 中引入和現在是推薦的方法,以激發 ETW 事件在應用程式中的繼承 (以前,您必須處理的手冊 ETW 清單生成)。 此外,EventSource 通過訂閱他們通過 EventListener,.NET 框架 4.5 (更多關於這瞬間) 中的新功能還允許消費的過程中,發生的事件。

可以按名稱或 GUID 標識的事件提供程式。 每個特定事件種類是反過來確定的事件 ID 和可選的關鍵字來區分其他無關 (TplEtwProvider 不使用關鍵字) 此提供程式激發的事件的類型。 有可選的任務和操作碼參數,您可能會發現有用的篩選,但我會單靠事件 id。 每個事件還定義詳細的級別。

協力廠商物流的事件有各種各樣的除了因果關係鏈,例如跟蹤的任務飛行、 遙測等用途。 雖然他們別為自訂的 awaitables,火。

介紹 EventListener

在.NET Framework 4 中,以捕獲 ETW 事件,您不得不將運行進程外 ETW 偵聽程式,如 Windows 性能記錄器或萬斯臣 PerfView,以及與您在調試器中觀察到的狀態然後關聯捕獲的資料。 這構成額外的問題,因為資料存儲在進程的記憶體空間外和崩潰轉儲並不包括它,使此解決方案驗屍調試不太適合。 例如,如果您依賴于 Windows 錯誤報告功能,以提供轉儲,你不會得到任何 ETW 跟蹤並因此因果關係資訊將會丟失。

但是,.NET 框架 4.5 中開始,也可以訂閱到協力廠商物流活動 (和其他由 EventSource 繼承者激發的事件),請通過 System.Diagnostics.Tracing.EventListener (bit.ly/XJelwF)。 這允許捕獲和保存的進程的記憶體空間中的堆疊追蹤段。 因此,一個小型的堆轉儲應該足以提取因果關係的資訊。 在本文中,我將只詳細資訊基於 EventListener 的訂閱。

值得一提的一個進程外攔截器的優點您總是可以獲得的呼叫堆疊的堆疊 ETW 事件 (依賴于現有的工具或做單調乏味的堆疊遍歷和跟蹤自己的模組位址) 聽。 訂閱使用 EventListener 事件,當你進不去的呼叫堆疊資訊 Windows 存儲區的應用程式,因為 StackTrace API 禁止。 (適用于 Windows 存儲應用程式的方法被介紹以後。

要訂閱事件,您必須從事件繼承­攔截器,寫 OnEventSourceCreated 方法,並確保您的攔截器實例獲取創建在您的程式的每個 AppDomain (訂閱是每個應用程式域中)。 EventListener 具現化之後,將調用此方法來通知正在創建的事件源的攔截器。 它還將提供創建攔截器之前存在的所有事件源的通知。 過濾事件源可以按名稱或 GUID (performance-wise,比較的 Guid 是一個好主意) 後, 調用 EnableEvents 訂閱源的攔截器:

private static readonly Guid tplGuid =
  new Guid("2e5dba47-a3d2-4d16-8ee0-6671ffdcd7b5");
protected override void OnEventSourceCreated(EventSource eventSource)
{
  if (eventSource.Guid == tplGuid)
    EnableEvents(eventSource, EventLevel.LogAlways);
}

若要處理的事件,您需要實現抽象方法 OnEventWritten。 保存和恢復堆疊追蹤部分,您需要捕獲呼叫堆疊右前安排了一個非同步作業,以及當它開始執行,然後,與之關聯的存儲的堆疊追蹤段。 要關聯這兩個事件,您可以使用 TaskID 參數。 裝箱到唯讀物件集合參數傳遞給事件源中的相應事件觸發方法並將其作為有效載荷的 EventWrittenEventArgs 屬性傳遞。

有趣的是,有特別快速路徑為 EventSource 事件所消耗 ETW (而不是通過 EventListener),作為在拳擊不會發生他們的論點。 這確實提供了一個性能的改進,但大多是瞄準了由於跨進程機械。

在 OnEventWritten 方法中,您需要區分事件源 (在您訂閱多個的情況),並確定這一事件本身。 堆疊追蹤將捕獲 (存儲) 時觸發 TaskScheduled 或 TaskWaitBegin 的事件,和關聯一個新開工的非同步作業 (還原) 在 TaskWaitEnd 中。 您還需要在相關識別碼作為 taskId 中傳遞。 圖 1 顯示如何將處理事件的輪廓。

圖 1 的 OnEventWritten Method 中的協力廠商物流事件的處理

protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
  if (eventData.EventSource.Guid == tplGuid)
  {
    int taskId;
    switch (eventData.EventId)
    {
      case 7: // Task scheduled
        taskId = (int)eventData.Payload[2];
        stackStorage.StoreStack(taskId);
        break;
      case 10: // Task wait begin
        taskId = (int)eventData.Payload[2];
        bool waitBehaviorIsSynchronous =
          (int)eventData.Payload[3] == 1;
        if (!waitBehaviorIsSynchronous)
          stackStorage.StoreStack(taskId);
        break;
      case 11: // Task wait end
        taskId = (int)eventData.Payload[2];
        stackStorage.RestoreStack(taskId);
        break;
    }
  }
}

附註:在代碼中的顯式值 ("幻數") 是不好的程式設計做法和在這裡僅用於簡潔。 隨附的示例代碼專案有他們方便的結構常數和枚舉,以避免重複和打字錯誤的風險。

請注意在 TaskWaitBegin,我檢查為 TaskWaitBehavior 正在同步的這種情況發生時正在等待的任務同步執行或已經完成。 在這種情況下,同步呼叫堆疊是仍在的地方,因此它不需要顯式存儲。

非同步本機存放區

無論您選擇保留呼叫堆疊段的資料結構需要以下品質:應保留存儲的值 (因果關係鏈),為每一個非同步作業,下列控制流跨一路上等待邊界和延續,同時牢記,延續可能在不同的執行緒上執行。

這表明一個類似于執行緒的本地變數,將維護有關當前的非同步作業 (鏈的延續),其值,而不是特定的執行緒。 大致可以命名"非同步本機存放區"。

CLR 已稱為 ExecutionCoNtext,已在一個執行緒上捕獲和還原另一方面 (其中繼續獲取執行),因此正在傳遞和控制流的資料結構。 這是本質上是一個容器,存儲可能需要繼續執行完全相同的環境,在那裡它們被中斷其他上下文 (SynchronizationCoNtext、 CallCoNtext 等等)。 StephenToub 已詳情,請致電 bit.ly/M0amHk。 最重要的是,您可以在其中似乎適合上述目的 (通過調用靜態方法中 LogicalSetData 和 LogicalGetData),調用上下文中存儲任意資料。

銘記這 CallCoNtext (其實,內部有兩個:LogicalCallCoNtext 和 IllogicalCallCoNtext) 是一個沉重的物件,旨在跨越遠端處理邊界流。 沒有自訂資料存儲時,運行時不會初始化上下文,盤與控制流保持它們的成本。 儘快調用 CallCoNtext.LogicalSetData 方法時,可變的 ExecutionCoNtext 和幾個雜湊表已創建並傳遞或從此克隆。

不幸的是,ExecutionCoNtext (以及所有其成分) 與之前所述的協力廠商物流事件消防捕獲並不久恢復。 因此,保存在 CallCoNtext 之間的任何自訂資料將被丟棄,ExecutionCoNtext 還原後,這將使它不適合我們特定的目的。

此外,調用類中不可用 Windows 存儲區為.NET 應用程式子集,所以這種情況下需要另一種方法。

生成非同步本機存放區,將解決這些問題的一種方法是同步的部分代碼執行時,使用執行緒本機存放區 (TLS) 中維持的價值。 然後,會觸發 TaskWaitStart 事件,當共用的 (非 TLS) 字典,由 TaskID 鍵控中存儲值。 時對應的事件,TaskWaitEnd,觸發時,從字典中移除的保留的值並將其保存回 TLS,可能在不同的執行緒上。

您可能知道,執行緒返回到執行緒池並獲取執行的新工作後,仍保留在 TLS 中存儲的值。 所以,有些時候,價值已從 TLS 中刪除 (否則,一些其他非同步作業執行緒上執行此稍後可能訪問猶如它是自己以前操作所存儲的值)。 因為嵌套的情況下,不能執行此 TaskWaitBegin 事件處理常式中操作等待,TaskWaitBegin 和 TaskWaitEnd 事件發生多次,等待,每一次和一個存儲的值,可能需要在之間,如下面的程式碼片段:

async Task OuterAsync()
{
  await InnerAsync();
}
async Task InnerAsync()
{
  await Task.Delay(1000);
}

相反,它是安全考慮在 TLS 中值是有資格被清除當前的非同步作業不再在一個執行緒上執行時。 因為 CLR 沒有在-­將通知的迴圈再用回執行緒池中的執行緒的進程事件 (有 ETW 之一 — —bit.ly/ZfAWrb),為此目的,我將使用 ThreadPoolDequeueWork FrameworkEventSource (也無證),由激發的執行緒池執行緒上開始新的操作時發生。 這便排除了非彙集的執行緒,要手動清理 TLS 中的,例如當 UI 執行緒返回到消息迴圈。

這一概念與捕獲堆疊段和串聯的工作實現,請參閱附帶的原始程式碼代碼下載中的 StackStorage 類。 也是一個清潔的抽象,AsyncLocal <T>,這使您可以存儲任何值,並將它與控制流轉移到後續非同步延續。 我將使用它作為因果關係鏈存儲為 Windows 存儲應用程式方案。

跟蹤在 Windows 存儲應用程式中的因果關係

所述的方法將仍然舉起 Windows 存儲方案中如果 System.Diagnostics.StackTrace API 都可用。 為好或壞,不是,這意味著您無法得到任何資訊調用代碼內當前上面的堆疊幀。 因此,儘管仍支援協力廠商物流的事件,對 TaskWaitStart 或 TaskWaitEnd 的調用是深埋在框架方法調用,所以您對您造成這些事件觸發的代碼沒有任何資訊。

幸運的是,Windows 存儲區為.NET 應用程式 (以及.NET 框架 4.5) 提供了 CallerMemberNameAttribute (bit.ly/PsDH0p) 和其同行 CallerFilePathAttribute 和 CallerLine­NumberAttribute。 當裝飾著這些可選方法參數時,編譯器會在編譯時初始化具有相應值的參數。 例如,下面的代碼將輸出"在行 14"c:\Full\Path\To\Program.cs 中的 main ():

static void Main(string[] args)
{
  LogCurrentFrame();
}
static void LogCurrentFrame([CallerMemberName] string name = null,
  [CallerFilePath] string path = null, 
    [CallerLineNumber] int line = 0)
{
  Console.WriteLine("{0}() in {1} at line {2}", name, path, line);
}

只有這樣,要獲取調用幀,這意味著您必須確保從您想要捕獲的因果關係鏈中的所有方法調用它獲取有關資訊的測井方法。 一個方便的位置,為此將裝飾每個等待調用擴充方法,像這樣的運算式:

await WorkAsync().WithCausality();

在這裡,WithCausality 方法捕獲當前幀,將其追加到因果關係鏈和返回任務或 awaitable (取決於何種 WorkAsync 返回),原單完成後,從因果關係鏈中刪除圖文框。

可以等待多個不同的東西,應該是 WithCausality 的多個重載。 這是簡單的任務 <T> (和更容易的任務):

public static Task<T> WithCausality<T>(this Task<T> task,
  [CallerMemberName] string member = null,
  [CallerFilePath] string file = null,
  [CallerLineNumber] int line = 0)
{
  var removeAction =
    AddFrameAndCreateRemoveAction(member, file, line);
  return task.ContinueWith(t => { removeAction(); return t.Result; });
}

然而,它是複雜的自訂 awaitables。 您可能知道,C# 編譯器允許您等待任何遵循特定模式的類型的實例 (見 bit.ly/AmAUIF),這使得將容納任何自訂 awaitable 不可能使用的寫作重載靜態只鍵入。 您可以為預定義的框架,如 YieldAwaitable 或 ConfiguredTaskAwaitable 中的 awaitables 的幾個快捷方式重載 — — 或在您的解決方案中定義的 — — 但一般你必須求助於動態語言運行時 (DLR)。 處理所有個案需要大量的樣板代碼,所以想看看附帶的原始程式碼的詳細資訊。

此外值得注意的情況下嵌套等待、 WithCausality 方法將執行從內到外 (等待計算運算式),因此,必須注意要裝配的堆疊中按正確的順序。

查看因果關係鏈

這兩種描述的方法的呼叫堆疊段或幀清單作為在記憶體中保留因果關係資訊。 然而,走他們和串聯成單一因果關係鏈的顯示是用手做很繁瑣的事。

最簡單的選項來自動化這是利用調試器計算機。 在這種情況下,在一個公共的類,作者公共靜態屬性 (或方法),在調用時,走的存儲段的清單並返回串聯的因果關係鏈。 然後您可以在調試過程中計算此屬性和文本視覺化檢視中查看結果。

不幸的是,這種方法不會在兩種情況下工作。 一個時發生的最頂層的堆疊幀中是相當常見的情況,為調試應用程式掛起,因為基於內核的同步基元做調用到本機代碼中的本機代碼。 調試器計算機將只顯示,"無法計算運算式,因為當前方法的代碼進行了優化"(Mike 檔描述這些限制在詳細 bit.ly/SLlNuT)。

另一個問題是驗屍的調試。 其實可以在Visual Studio中打開一個小型轉儲和令人驚訝 (鑒於有沒有進程進行調試,只有其記憶體傾印),就可以檢查 (運行屬性 getter) 的屬性值和甚至調用某些方法 ! 這神奇的功能內置Visual Studio調試器和工程通過解釋監視運算式,它 (在對比度到即時調試,編譯後的代碼獲取執行所在) 調用到的所有方法。

很明顯,有一些限制。 例如,同時做轉儲調試,您不能在任何方式調用到本機方法 (意味著你甚至不能執行的委託,因為其 Invoke 方法生成的本機代碼) 或訪問一些限制 Api (如 System.Reflection)。 基於解譯器的評價也是預計緩慢 — — 和可悲的是,由於一個 bug,調試轉儲的評價超時是限於 1 秒Visual Studio2012 年,不管配置中。 這一點,因為需要遍歷堆疊追蹤段的清單,並逐一查看所有幀的方法調用的數目禁止為此目的的計算機使用。

幸運的是,調試器始終允許訪問的欄位值 (即使在調試轉儲或頂部堆疊幀時在本機代碼中),使其得以通過構成存儲的因果關係鏈的物件進行爬網並重建它。 這是很明顯是冗長乏味的所以我寫了一個Visual Studio擴展,這會為您 (見所附的示例代碼)。 圖 2 顯示最後的經驗是什麼樣子。 請注意右邊的圖也由該擴展生成相當於非同步並行堆疊。

Causality Chain for an Asynchronous Method and “Parallel” Causality for All Threads
圖 2 非同步方法和"平行"的因果關係的所有線程的因果關係鏈

比較和警告

這兩種因果關係跟蹤方法不是免費的。 (調用者資訊-基於) 第二個是更輕量的因為它不涉及昂貴的 StackTrace API,而依賴編譯器將調用方提供幀資訊在編譯時,在正在運行的程式中的"免費"的手段。 但是,它仍然使用事件處理基礎結構與其成本來支援 AsyncLocal <T>。 另一方面,第一種方法提供了更多的資料,不跳過幀而不等待。 它還會自動跟蹤幾個等待基於任務的非同步凡沒有出現其他情況,如 Task.Run 方法 ; 另一方面,它不能與使用自訂的 awaitables。

TPL 基於事件的跟蹤器的另一個好處是,現有的非同步代碼不需要修改,而對的調用方資訊基於屬性的方法,則需要更改每個等待您的程式中的語句。 但只有後者支援 Windows 存儲應用程式。

TPL 事件跟蹤程式也患有大量的樣板框架代碼的堆疊追蹤的細分,雖然它可以輕鬆地篩選由框架的命名空間或類的名稱。 請參閱有關的常用的篩選器清單的代碼示例。

另一個涉及非同步代碼中的迴圈。 請考慮下面的程式碼片段:

async static Task Loop()
{
  for (int i = 0; i < 10; i++)
  {
    await FirstAsync();
    await SecondAsync();
    await ThirdAsync();
  }
}

由方法的末尾,其因果關係鏈會增長到超過 30 段、 FirstAsync、 SecondAsync 和 ThirdAsync 的幀之間反復交替。 對於有限迴圈,這可能是可以忍受的儘管它仍然是浪費記憶體來存儲重複幀 10 倍。 但是,在某些情況下,一個程式可能引入有效的無限迴圈,例如,在一個消息迴圈的情況下。 此外,無限重複可能會引入不迴圈或等待構造 — — 本身重新上每個刻度線計時器是一個完美的例子。 跟蹤無限的因果關係鏈是運行記憶體不足,因此存儲的資料量已經以某種方式將減至一個有限的數額確定方法。

這一問題並不影響基於調用方的資訊追蹤器中,因為它幀從清單中刪除後立即開始的延續。 有兩種 (可組合) 的方法來修復此問題為協力廠商物流活動方案。 一是要削減較舊的資料基於滾動的最大存儲量。 另一個是有效地代表迴圈和避免重複。 兩種做法,也可能檢測常見的無限迴圈模式和削減的因果關係鏈明確這些點。

隨意請參閱隨附的示例專案,以查看如何執行迴圈折疊。

如上所述,協力廠商物流事件 API 僅允許您捕獲一條因果關係鏈,不圖。 這是因為 Task.WaitAll 和 Task.WhenAll 方法實現為倒計時,只有當最後一項任務來完成,該計數器變為零,安排延續。 因此,只有最後一個完成的任務形成因果關係鏈。

總結

在本文中,您已經瞭解呼叫堆疊、 返回堆疊和因果關係鏈之間的差異。 現在應該意識到,.NET 框架提供了跟蹤調度的擴充點和非同步作業的執行,能夠利用這些來捕獲和保留的因果關係鏈。 方法描述蓋住跟蹤中經典和 Windows 存儲區的應用程式,在因果關係和驗屍調試方案。 您還學習了有關非同步本機存放區的概念和其可能的實現用於 Windows 存儲區的應用程式。

現在去和納入跟蹤到非同步代碼庫的因果關係或使用非同步本機存放區區中平行計算 ; 探討.NET 框架 4.5 和.NET 為 Windows 存儲應用程式提供建造一些新的例如跟蹤程式在您的程式 ; 未完成任務的事件源 或使用此擴充點來激發您自己的事件來調整應用程式的性能。

Andriy (Andrew) Stasyuk 是在 Microsoft 託管語言團隊測試二、 軟體發展工程師。他有七年的經驗作為參與者、 任務作者、 陪審團成員和在不同的國家和國際程式設計競賽教練。他在 Paladyne/Broadridge 財務解決方案公司的財務軟體發展工作 和德意志銀行才搬到 Microsoft。他在節目中的主要利益是演算法、 並行和皮包公司。

感謝以下技術專家對本文的審閱:萬斯摩理臣和盧西恩 Wischik