共用方式為


偵錯非同步應用程式

本教學課程示範如何使用 [平行堆疊] 視窗的 [工作] 檢視來偵錯 C# 非同步應用程式。 此視窗可協助您瞭解和驗證使用非同步/等待模式 ( 也稱為工作型非同步模式 (TAP) ) 的程式碼執行階段行為。

對於使用工作平行程式庫 (TPL) 但未使用非同步/等待模式的應用程式,或使用並行執行階段的 C++ 應用程式,請使用 [平行堆疊] 視窗中的 [執行緒] 檢視進行偵錯。 如需詳細資訊,請參閱 偵錯死結在 [平行堆疊] 視窗中檢視執行緒和工作

「任務」視圖可協助您:

  • 檢視使用非同步/等待模式之應用程式的呼叫堆疊視覺效果。 在這些案例中,[工作] 檢視會提供更完整的應用程式狀態圖片。

  • 識別已排程執行但尚未執行的非同步程式碼。 例如,未傳回任何資料的 HTTP 要求更有可能顯示在「作業」視圖中,而不是「執行緒」視圖中,這可協助您隔離問題。

  • 協助識別問題,例如同步-非同步模式,以及與潛在問題相關的提示,例如封鎖或等待的任務。 sync-over-async 程式碼模式是指以同步方式呼叫非同步方法的程式碼,已知會封鎖執行緒,而且是執行緒儲存區耗盡的最常見原因。

非同步呼叫堆疊

平行堆疊中的 [工作] 檢視提供非同步呼叫堆疊的視覺效果,因此您可以查看應用程式中正在發生 (或應該發生) 的情況。

以下是在「任務」檢視中解譯資料時要記住的幾個重要要點。

  • 非同步呼叫堆疊是邏輯或虛擬呼叫堆疊,而不是代表堆疊的實體呼叫堆疊。 使用非同步程式碼時 (例如,使用 await 關鍵字) 時,偵錯工具會提供「非同步呼叫堆疊」或「虛擬呼叫堆疊」的檢視。 非同步呼叫堆疊與執行緒型呼叫堆疊或「實體堆疊」不同,因為非同步呼叫堆疊目前不一定在任何實體執行緒上執行。 相反地,非同步呼叫堆疊是未來將以非同步方式執行的程式碼的延續或「承諾」。 呼叫堆疊是使用 接續來建立。

  • 已排程但目前未執行的非同步程式碼不會出現在實體呼叫堆疊上,但應該出現在 [工作] 檢視的非同步呼叫堆疊上。 如果您使用像 .Wait.Result 等方法封鎖執行緒,您可能會在實體呼叫堆疊中看到程式碼。

  • 非同步虛擬調用堆疊未必是直觀的,這是因為使用諸如 .WaitAny.WaitAll 這樣的方法調用會造成分支。

  • [ 呼叫堆疊] 視窗與 [工作] 檢視結合使用可能很有用,因為它會顯示目前執行執行緒的實體呼叫堆疊。

  • 虛擬呼叫堆疊的相同區段會分組在一起,以簡化複雜應用程式的視覺效果。

    下列概念動畫顯示如何將分組套用至虛擬呼叫堆疊。 只有虛擬呼叫堆疊的相同區段才會分組。 將滑鼠停留在群組的呼叫堆疊上,以識別執行工作的執行緒。

    虛擬呼叫堆疊分組的圖解。

C# 範例

本逐步解說中的範例程式碼適用於模擬大猩猩一天生活的應用程式。 練習的目的是瞭解如何使用 [平行堆疊] 視窗的 [工作] 檢視來偵錯非同步應用程式。

此範例展示了使用同步-異步反模式,這可能會導致執行緒集區耗盡。

為了讓呼叫堆疊直觀,範例應用程式會執行下列循序步驟:

  1. 建立代表大猩猩的物件。
  2. 大猩猩醒來了。
  3. 大猩猩早上散步。
  4. 大猩猩在叢林中發現了香蕉。
  5. 大猩猩吃東西。
  6. 大猩猩從事猴子生意。

建立範例專案

  1. 開啟 Visual Studio 並建立新的專案。

    如果啟動視窗未開啟,請選擇 [ 檔案>開始視窗]。

    在 [開始] 視窗中,選擇 [ 新增專案]。

    在 [ 建立新專案 ] 視窗中,於搜尋方塊中輸入或輸入 控制台 。 接下來,從 [語言] 列表中選擇 [C#],然後選擇 [平臺] 清單中的 [Windows]。

    套用語言和平台篩選條件之後,請選擇適用於 .NET 的 主控台應用程式 ,然後選擇 [ 下一步]。

    備註

    如果您沒有看到正確的範本,請移至 [工具>取得工具和功能...],這會開啟Visual Studio安裝程式。 選擇 [.NET 桌面開發] 工作負載,然後選擇 [修改]

    在 [ 設定新專案 ] 視窗中,輸入名稱或使用 [ 專案名稱 ] 方塊中的預設名稱。 接著,選擇 [下一步]

    針對 .NET,選擇建議的目標架構或 .NET 8,然後選擇 [建立]。

    新的主控台專案隨即出現。 建立項目之後,就會顯示原始程序檔。

  2. 開啟專案中的 .cs 程式碼檔案。 刪除其內容以建立空的程式代碼檔案。

  3. 將所選語言的下列程式代碼貼到空的程式代碼檔案中。

    using System.Diagnostics;
    
    namespace AsyncTasks_SyncOverAsync
    {
         class Jungle
         {
             public static async Task<int> FindBananas()
             {
                 await Task.Delay(1000);
                 Console.WriteLine("Got bananas.");
                 return 0;
             }
    
             static async Task Gorilla_Start()
             {
                 Debugger.Break();
                 Gorilla koko = new Gorilla();
                 int result = await Task.Run(koko.WakeUp);
             }
    
             static async Task Main(string[] args)
             {
                 List<Task> tasks = new List<Task>();
                 for (int i = 0; i < 2; i++)
                 {
                     Task task = Gorilla_Start();
                     tasks.Add(task);
    
                 }
                 await Task.WhenAll(tasks);
    
             }
         }
    
         class Gorilla
         {
    
             public async Task<int> WakeUp()
             {
                 int myResult = await MorningWalk();
    
                 return myResult;
             }
    
             public async Task<int> MorningWalk()
             {
                 int myResult = await Jungle.FindBananas();
                 GobbleUpBananas(myResult);
    
                 return myResult;
             }
    
             /// <summary>
             /// Calls a .Wait.
             /// </summary>
             public void GobbleUpBananas(int food)
             {
                 Console.WriteLine("Trying to gobble up food synchronously...");
    
                 Task mb = DoSomeMonkeyBusiness();
                 mb.Wait();
    
             }
    
             public async Task DoSomeMonkeyBusiness()
             {
                 Debugger.Break();
                 while (!System.Diagnostics.Debugger.IsAttached)
                 {
                     Thread.Sleep(100);
                 }
    
                 await Task.Delay(30000);
                 Console.WriteLine("Monkey business done");
             }
         }
    }
    

    更新程式代碼檔案之後,請儲存變更並建置方案。

  4. 在 [File] \(檔案\) 功能表上,選取 [Save All] \(全部儲存\)

  5. 在 [建置] 功能表上,選取 [建置方案]

使用 [平行堆疊] 視窗中的 [工作檢視]

  1. [偵錯] 功能表上,選取 [開始偵錯 ] (或 F5),然後等候第一個 Debugger.Break() 命中。

  2. F5 一次,偵錯工具會在相同的 Debugger.Break() 行上再次暫停。

    這會在第二個 Gorilla_Start非同步工作中發生的第二次呼叫中暫停。

  3. 選取 偵錯 > Windows > 平行堆疊 以開啟平行堆疊視窗,然後從視窗中的 檢視 下拉式清單中選取 工作

    平行堆疊視窗中「工作」檢視的螢幕擷取畫面。

    請注意,非同步呼叫堆疊的標籤會說明 2 個非同步邏輯堆疊。 上次按 F5 時,您開始了另一項工作。 為了簡化複雜應用程式,相同的非同步呼叫堆疊會分組為單一視覺化表示法。 這提供了更完整的信息,特別是在具有許多任務的場景中。

    相較於 [工作] 檢視,[ 呼叫堆疊] 視窗只會顯示目前執行緒的呼叫堆疊,而不是多個工作的呼叫堆疊。 將它們一起查看通常很有幫助,以便更全面地了解應用程序狀態。

    呼叫堆疊的螢幕擷取畫面。

    小提示

    [呼叫堆疊] 視窗可以使用描述 Async cycle來顯示一些資訊,例如死結。

    在偵錯期間,您可以切換是否顯示外部程式碼。 若要切換此功能,請以滑鼠右鍵按一下 [呼叫堆疊] 視窗的 [名稱] 資料表標頭,然後選取或清除 [顯示外部程式碼]。 如果您顯示外部程式碼,您仍然可以使用本逐步解說,但您的結果可能與圖解不同。

  4. 再次按 F5 ,偵錯工具會在方法中 DoSomeMonkeyBusiness 暫停。

    F5 之後的 [工作] 檢視畫面的螢幕擷取畫面。

    將更多非同步方法新增至內部接續鏈結之後,該檢視會顯示更完整的非同步呼叫堆疊,這在使用 await 和類似方法時會發生。 DoSomeMonkeyBusiness 可能存在於非同步呼叫堆疊的頂端,也可能不存在,因為它是非同步方法,但尚未新增至接續鏈結。 我們將在以下步驟中探討為什麼會出現這種情況。

    此檢視也會顯示Jungle.MainStatus Blocked已封鎖圖示。 這很有參考價值,但通常並不表示有問題。 封鎖的任務是因為等待另一個任務完成、事件發出的訊號或釋放鎖而被阻擋的任務。

  5. 將滑鼠停留在方法上 GobbleUpBananas ,以取得執行工作的兩個執行緒的相關資訊。

    與呼叫堆疊相關聯的執行緒螢幕擷取畫面。

    目前的執行緒也會出現在 [偵錯] 工具列的 [執行緒] 清單中。

    偵錯工具列中目前執行緒的螢幕擷取畫面。

    您可以使用 [執行緒] 清單,將偵錯工具內容切換至不同的執行緒。

  6. 再次按 F5,偵錯工具會在 DoSomeMonkeyBusiness 方法中暫停第二個任務。

    第二個 F5 之後的任務視圖的屏幕截圖。

    視工作執行的時間而定,此時您會看到個別或群組的非同步呼叫堆疊。

    在上圖中,這兩個工作的非同步呼叫堆疊是分開的,因為它們並不相同。

  7. 再次按 F5 ,您會看到發生很長的延遲,而且 [工作] 檢視不會顯示任何非同步呼叫堆疊資訊。

    延遲是由長時間執行的任務所造成。 在此範例中,該程序會模擬一個長時間執行的任務,例如發出網頁請求的情境,這可能會導致執行緒池耗竭的情況發生。 [工作] 檢視中不會顯示任何內容,因為即使工作可能遭到封鎖,您目前也不會在偵錯工具中暫停。

    小提示

    [全部暫停] 按鈕是在發生死結或所有工作和執行緒目前遭到封鎖時取得呼叫堆疊資訊的好方法。

  8. 在 IDE 頂端的 [偵錯] 工具列中,選取 [全部中斷 ] 按鈕 (暫停圖示)、 Ctrl + Alt + 中斷

    選擇「全部中斷」後的「任務」視圖的螢幕截圖。

    在 [工作] 檢視中,您會在非同步呼叫堆疊的接近頂端處看到 GobbleUpBananas 已被封鎖。 事實上,兩個任務在同一點被阻塞。 被封鎖的工作不一定是意外的,也不一定表示有問題。 不過,觀察到的執行延遲表示有問題,而此處的呼叫堆疊資訊會顯示問題的位置。

    在上一個螢幕擷取畫面的左側,捲曲的綠色箭號指出目前的偵錯工具內容。 兩個任務在mb.Wait()方法中被GobbleUpBananas封鎖。

    [呼叫堆疊] 視窗也會顯示目前的執行緒已封鎖。

    選擇 Break All 後呼叫堆疊的螢幕擷取畫面。

    Wait() 的呼叫會封鎖同步對 GobbleUpBananas 的呼叫中的執行緒。 這是同步覆蓋非同步反模式的範例,如果這種情況發生在 UI 執行緒或大型處理工作負載下,通常會使用await程式碼進行修正來解決。 如需詳細資訊,請參閱 偵錯執行緒集區耗竭。 若要使用分析工具來偵錯執行緒集區耗盡,請參閱 案例研究:隔離效能問題

    同樣值得關注的是, DoSomeMonkeyBusiness 不會出現在呼叫堆疊上。 它目前已排程,未執行,因此只會出現在 [工作] 檢視的非同步呼叫堆疊中。

    小提示

    偵錯工具會針對每個執行緒中斷以檢視程式碼。 例如,這表示如果您按 F5 繼續執行,當應用程式達到下一個中斷點時,可能會切換到在不同執行緒上執行的程式碼。 如果您需要管理此中斷點以進行偵錯,您可以新增其他中斷點、新增條件中斷點,或使用 [全部中斷]。 如需此行為的詳細資訊,請參閱 遵循具有條件式中斷點的單一執行緒

修正範例程式碼

  1. GobbleUpBananas 方法用下列程式碼取代。

     public async Task GobbleUpBananas(int food) // Previously returned void.
     {
         Console.WriteLine("Trying to gobble up food...");
    
         //Task mb = DoSomeMonkeyBusiness();
         //mb.Wait();
         await DoSomeMonkeyBusiness();
     }
    
  2. MorningWalk 方法中,使用 await 呼叫 GobbleUpBananas。

    await GobbleUpBananas(myResult);
    
  3. 選取 [ 重新啟動 ] 按鈕 (Ctrl + Shift + F5),然後按 F5 數次,直到應用程式顯示為「當機」。

  4. 全部中止

    這次, GobbleUpBananas 非同步執行。 當您中斷時,您會看到非同步呼叫堆疊。

    程式碼修正之後偵錯工具內容的螢幕擷取畫面。

    [呼叫堆疊] 視窗是空的,只有 ExternalCode 項目例外。

    程式碼編輯器不會向我們顯示任何內容,只是它提供了一條訊息,指出所有執行緒都在執行外部程式碼。

    不過,「工作」視圖確實提供有用的資訊。 DoSomeMonkeyBusiness 如預期的那樣,位於非同步呼叫堆疊的頂端。 這正確地告訴我們長時間運行的方法所在的位置。 這有助於在 [呼叫堆疊] 視窗中的實體呼叫堆疊未提供足夠的詳細數據時隔離非同步/等候問題。

總結

此逐步解說示範了 平行堆疊 偵錯工具視窗。 在使用 async/await 模式的應用程式上使用此視窗。