共用方式為


使用線程檢視對死結進行偵錯

本教學課程示範如何使用執行緒檢視的平行堆疊視窗來偵錯多執行緒應用程式。 此視窗可協助您了解及驗證多線程程式代碼的運行時間行為。

C#、C++ 和 Visual Basic 支援 [執行緒] 檢視。 提供 C# 和 C++ 的範例程式碼,但某些程式碼參考和插圖僅適用於 C# 範例程式碼。

[線程] 檢視可協助您:

  • 檢視多個線程的呼叫堆疊視覺效果,其提供比呼叫堆棧視窗更完整的應用程式狀態圖,而此視窗只會顯示目前線程的呼叫堆疊。

  • 協助識別封鎖或死結線程等問題。

多線程呼叫堆疊

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

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

呼叫堆棧群組的圖例。

範例程式碼概觀 (C#、C++)

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

此範例包含死鎖的示例,發生於兩個執行緒互相等待時。

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

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

建立範例專案

若要建立專案:

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

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

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

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

    套用語言和平台篩選條件之後,請選擇所選語言的 主控台應用程式 ,然後選擇 下一步

    Note

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

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

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

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

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

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

     using System.Diagnostics;
    
     namespace Multithreaded_Deadlock
     {
         class Jungle
         {
             public static readonly object tree = new object();
             public static readonly object banana_bunch = new object();
             public static Barrier barrier = new Barrier(2);
    
             public static int FindBananas()
             {
                 // Lock tree first, then banana
                 lock (tree)
                 {
                     lock (banana_bunch)
                     {
                         Console.WriteLine("Got bananas.");
                         return 0;
                     }
                 }
             }
    
             static void Gorilla_Start(object lockOrderObj)
             {
                 Debugger.Break();
                 bool lockTreeFirst = (bool)lockOrderObj;
                 Gorilla koko = new Gorilla(lockTreeFirst);
                 int result = 0;
                 var done = new ManualResetEventSlim(false);
    
                 Thread t = new Thread(() =>
                 {
                     result = koko.WakeUp();
                     done.Set();
                 });
                 t.Start();
                 done.Wait();
             }
    
             static void Main(string[] args)
             {
                 List<Thread> threads = new List<Thread>();
                 // Start two threads with opposite lock orders
                 threads.Add(new Thread(Gorilla_Start));
                 threads[0].Start(true);  // First gorilla locks tree then banana
                 threads.Add(new Thread(Gorilla_Start));
                 threads[1].Start(false); // Second gorilla locks banana then tree
    
                 foreach (var t in threads)
                 {
                     t.Join();
                 }
             }
         }
    
         class Gorilla
         {
             private readonly bool lockTreeFirst;
    
             public Gorilla(bool lockTreeFirst)
             {
                 this.lockTreeFirst = lockTreeFirst;
             }
    
             public int WakeUp()
             {
                 int myResult = MorningWalk();
                 return myResult;
             }
    
             public int MorningWalk()
             {
                 Debugger.Break();
                 if (lockTreeFirst)
                 {
                     lock (Jungle.tree)
                     {
                         Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample
                         Jungle.FindBananas();
                         GobbleUpBananas();
                     }
                 }
                 else
                 {
                     lock (Jungle.banana_bunch)
                     {
                         Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample
                         Jungle.FindBananas();
                         GobbleUpBananas();
                     }
                 }
                 return 0;
             }
    
             public void GobbleUpBananas()
             {
                 Console.WriteLine("Trying to gobble up food...");
                 DoSomeMonkeyBusiness();
             }
    
             public void DoSomeMonkeyBusiness()
             {
                 Thread.Sleep(1000);
                 Console.WriteLine("Monkey business done");
             }
         }
     }
    
  4. 在 [File] \(檔案\) 功能表上,選取 [Save All] \(全部儲存\)

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

使用 [平行堆棧] 視窗的 [線程] 檢視

若要開始偵錯:

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

    Note

    在 C++ 中,偵錯工具會在 __debug_break() 暫停。 本文中的其餘程式碼參考和插圖適用於 C# 版本,但相同的偵錯原則適用於 C++。

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

    這會在第二次呼叫 Gorilla_Start 時暫停,這發生在另一個執行緒中。

    Tip

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

  3. 選取 [偵>錯 Windows > 平行堆棧] 以開啟 [平行堆棧] 視窗,然後從視窗中的 [檢視] 下拉式清單中選取 [線程]。

    [平行堆棧] 視窗中 [線程] 檢視的螢幕快照。

    執行緒 檢視中,目前執行緒的堆疊框架和呼叫路徑會以藍色高亮顯示。 線程的目前位置是由黃色箭號顯示。

    請注意,Gorilla_Start 呼叫堆疊的標籤為 2 個執行緒。 當您上次按下 F5 時,會啟動另一個線程。 為了簡化複雜的應用程式,相同的呼叫堆疊會分組成單一視覺表示法。 這可簡化可能複雜的資訊,特別是在具有許多線程的案例中。

    在偵錯期間,您可以切換是否顯示外部程式碼。 若要切換功能,請選取或清除 [顯示外部程序代碼]。 如果您顯示外部程式碼,您仍然可以使用本逐步解說,但您的結果可能與圖解不同。

  4. 再次按下F5,調試器會在Debugger.Break()行中的MorningWalk方法暫停。

    [平行堆疊] 視窗會顯示 方法中 MorningWalk 目前執行線程的位置。

    F5 之後的 [線程] 檢視螢幕快照。

  5. 將滑鼠停留在 方法上 MorningWalk ,以取得群組呼叫堆疊所代表的兩個線程相關信息。

    與呼叫堆疊相關聯的線程螢幕快照。

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

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

    您可以使用 [執行緒] 清單,將偵錯工具內容切換至不同的執行緒。 這不會變更目前的執行線程,而只會變更調試程序內容。

    或者,您可以按兩下 [執行緒] 檢視中的方法,或在 [執行緒] 檢視中的方法上以滑鼠右鍵按一下,然後選取 切換至 Frame>[thread ID],以切換調試程序內容。

  6. 再次按 F5 ,調試程式會在 第二個線程的 MorningWalk 方法中暫停。

    第二個 F5 之後的 [線程] 檢視螢幕快照。

    視線程執行的時間而定,此時您會看到個別或群組的呼叫堆疊。

    在上圖中,兩個線程的呼叫堆疊被部分分組。 呼叫堆疊的相同區段會分組,而箭頭線會指向分隔的區段(也就是不完全相同)。 目前的堆疊框架以藍色突出顯示。

  7. 再次按 F5 ,您會看到發生長時間的延遲,而且 [線程] 檢視不會顯示任何呼叫堆棧資訊。

    延遲是由死結所造成。 線程檢視中不顯示任何項目,因為即使線程可能阻塞,您目前並未在調試器中暫停。

    Note

    在 C++ 中,您也會看到偵錯錯誤,指出已呼叫 abort()

    Tip

    如果發生死結或所有線程目前被封鎖,[全部中斷] 按鈕是取得呼叫堆疊資訊的好方法。

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

    選取 [全部中斷] 後的 [Threads] 檢視螢幕截圖。

    執行緒檢視中的呼叫堆疊頂端顯示 FindBananas 已發生死鎖。 中的 FindBananas 執行指標是捲曲的綠色箭號,表示目前的調試程序內容,但也告訴我們線程目前未執行。

    Note

    在 C++ 中,您看不到有用的「檢測到死結」圖示和資訊。 然而,你仍然會在Jungle.FindBananas中找到捲曲的綠色箭頭,暗示著死鎖的位置。

    在程式代碼編輯器中,我們發現函式中的 lock 捲曲綠色箭號。 在lock 方法中的 FindBananas 函數上,這兩個執行緒被阻塞。

    選取 [全部中斷] 之後的程式代碼編輯器螢幕快照。

    根據執行緒執行的順序,死結會出現在lock(tree)語句或lock(banana_bunch)語句中。

    呼叫 lock 會封鎖 方法中的 FindBananas 線程。 一個線程正在等待另一個線程釋放tree的鎖,但另一個線程正在等待釋放鎖定banana_bunch,才能釋放tree的鎖。 這是當兩個線程互相等候時所發生的傳統死結範例。

    如果您使用 Copilot,您也可以取得 AI 產生的線程摘要,以協助識別潛在的死結。

    Copilot 線程摘要描述的螢幕快照。

修正範例程式碼

若要修正此程式碼,請確保在所有執行緒內一致且全域順序地取得多個鎖定。 這可防止迴圈等候並消除死結。

  1. 若要修正死結,請將 中的 MorningWalk 程式代碼取代為下列程序代碼。

    public int MorningWalk()
    {
        Debugger.Break();
        // Always lock tree first, then banana_bunch
        lock (Jungle.tree)
        {
            Jungle.barrier.SignalAndWait(5000); // OK to remove
            lock (Jungle.banana_bunch)
            {
                Jungle.FindBananas();
                GobbleUpBananas();
            }
        }
        return 0;
    }
    
  2. 重新啟動應用程式。

Summary

此逐步解說示範了 平行堆疊 偵錯工具視窗。 在使用多線程程式代碼的實際專案上使用這個視窗。 您可以檢查以 C++、C# 或 Visual Basic 撰寫的平行程式代碼。