逐步解說:在 Visual Studio (C#、Visual Basic、C++) 中偵錯平行應用程式

本逐步解說顯示如何使用 [平行工作] 和 [平行堆疊] 視窗來偵錯平行應用程式。 這些視窗協助您了解和驗證使用工作平行程式庫 (TPL)並行執行階段的程式碼之執行階段行為。 本逐步解說提供具有內建中斷點的範例程式碼。 在程式碼中斷之後,本逐步解說會顯示如何使用 [平行工作] 和 [平行堆疊] 視窗來檢查程式碼。

本逐步解說教導下列工作:

  • 如何在一個檢視中檢視所有執行緒的呼叫堆疊。

  • 如何檢視應用程式中建立之 System.Threading.Tasks.Task 執行個體的清單。

  • 如何檢視工作而非執行緒的實際呼叫堆疊。

  • 如何從 [平行工作] 和 [平行堆疊] 視窗巡覽至程式碼。

  • 視窗如何透過分組、縮放和其他相關功能來處理比例調整。

必要條件

本逐步解說假定已啟用 Just My Code (在 Visual Studio 的較新版本中,預設會啟用此功能)。 選取 [工具] 功能表上的 [選項],展開 [偵錯] 節點,再選取 [一般],然後選取 [啟用 Just My Code (僅限受授程式碼)]。 如果未設定這項功能,您仍然可以使用本逐步解說,但結果可能與插圖不同。

C# 範例

如果您使用 C# 範例,本逐步解說也會假設外部程式碼已隱藏。 若要切換是否顯示外部程式碼,請以滑鼠右鍵按一下 [呼叫堆疊] 視窗的 [名稱] 表格標題,然後選取或清除 [顯示外部程式碼]。 如果未設定這項功能,您仍然可以使用本逐步解說,但結果可能與插圖不同。

C++ 範例

如果您使用 C++ 範例,則可以忽略本文中對於外部程式碼的引述。 外部程式碼只適用於 C# 範例。

插圖

本文中的插圖是在執行 C# 範例的四核心電腦上錄製。 雖然您可以使用其他組態來完成本逐步解說,但插圖可能與您電腦上所呈現的畫面不同。

建立範例專案

本逐步解說中的範例程式碼適用於不執任何動作的應用程式。 練習的目的是要瞭解如何使用工具視窗來偵錯平行應用程式。

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

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

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

    在開始視窗中,選擇 [建立新專案]

    在 [建立新專案] 視窗的搜尋方塊中輸入或鍵入 ASP.NET。 接下來,從語言清單中選擇 C#C++Visual Basic,然後從平台清單中選擇 Windows

    套用語言和平台篩選後,為 .NET Core 或 C++ 選擇主控台應用程式,然後選擇 [下一步]

    注意

    如果看不到正確的範本,請移至 [工具]>[取得工具和功能...],這將開啟 Visual Studio 安裝程式。 選擇 [NET 桌面開發] 或 [使用 C++ 的桌面開發] 工作負載,然後選擇 [修改] 按鈕。

    在 [設定新專案] 視窗中,在 [專案名稱] 方塊中輸入名稱或使用預設名稱。 然後,選擇 [下一步] 或 [建立],無論哪個選項可用。

    對於 .NET Core,請選擇建議的目標框架或 .NET 8,然後選擇 [建立]

    新的主控台專案隨即出現。 建立專案之後,便會出現來源檔案。

  2. 在專案中開啟 .cpp、.cs 或 .vb 程式碼檔案。 刪除其內容,建立空白程式碼檔案。

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

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Diagnostics;
    
    class S
    {
      static void Main()
      {
        pcount = Environment.ProcessorCount;
        Console.WriteLine("Proc count = " + pcount);
        ThreadPool.SetMinThreads(4, -1);
        ThreadPool.SetMaxThreads(4, -1);
    
        t1 = new Task(A, 1);
        t2 = new Task(A, 2);
        t3 = new Task(A, 3);
        t4 = new Task(A, 4);
        Console.WriteLine("Starting t1 " + t1.Id.ToString());
        t1.Start();
        Console.WriteLine("Starting t2 " + t2.Id.ToString());
        t2.Start();
        Console.WriteLine("Starting t3 " + t3.Id.ToString());
        t3.Start();
        Console.WriteLine("Starting t4 " + t4.Id.ToString());
        t4.Start();
    
        Console.ReadLine();
      }
    
      static void A(object o)
      {
        B(o);
      }
      static void B(object o)
      {
        C(o);
      }
      static void C(object o)
      {
        int temp = (int)o;
    
        Interlocked.Increment(ref aa);
        while (aa < 4)
        {
          ;
        }
    
        if (temp == 1)
        {
          // BP1 - all tasks in C
          Debugger.Break();
          waitFor1 = false;
        }
        else
        {
          while (waitFor1)
          {
            ;
          }
        }
        switch (temp)
        {
          case 1:
            D(o);
            break;
          case 2:
            F(o);
            break;
          case 3:
          case 4:
            I(o);
            break;
          default:
            Debug.Assert(false, "fool");
            break;
        }
      }
      static void D(object o)
      {
        E(o);
      }
      static void E(object o)
      {
        // break here at the same time as H and K
        while (bb < 2)
        {
          ;
        }
        //BP2 - 1 in E, 2 in H, 3 in J, 4 in K
        Debugger.Break();
        Interlocked.Increment(ref bb);
    
        //after
        L(o);
      }
      static void F(object o)
      {
        G(o);
      }
      static void G(object o)
      {
        H(o);
      }
      static void H(object o)
      {
        // break here at the same time as E and K
        Interlocked.Increment(ref bb);
        Monitor.Enter(mylock);
        while (bb < 3)
        {
          ;
        }
        Monitor.Exit(mylock);
    
    
        //after
        L(o);
      }
      static void I(object o)
      {
        J(o);
      }
      static void J(object o)
      {
        int temp2 = (int)o;
    
        switch (temp2)
        {
          case 3:
            t4.Wait();
            break;
          case 4:
            K(o);
            break;
          default:
            Debug.Assert(false, "fool2");
            break;
        }
      }
      static void K(object o)
      {
        // break here at the same time as E and H
        Interlocked.Increment(ref bb);
        Monitor.Enter(mylock);
        while (bb < 3)
        {
          ;
        }
        Monitor.Exit(mylock);
    
    
        //after
        L(o);
      }
      static void L(object oo)
      {
        int temp3 = (int)oo;
    
        switch (temp3)
        {
          case 1:
            M(oo);
            break;
          case 2:
            N(oo);
            break;
          case 4:
            O(oo);
            break;
          default:
            Debug.Assert(false, "fool3");
            break;
        }
      }
      static void M(object o)
      {
        // breaks here at the same time as N and Q
        Interlocked.Increment(ref cc);
        while (cc < 3)
        {
          ;
        }
        //BP3 - 1 in M, 2 in N, 3 still in J, 4 in O, 5 in Q
        Debugger.Break();
        Interlocked.Increment(ref cc);
        while (true)
          Thread.Sleep(500); // for ever
      }
      static void N(object o)
      {
        // breaks here at the same time as M and Q
        Interlocked.Increment(ref cc);
        while (cc < 4)
        {
          ;
        }
        R(o);
      }
      static void O(object o)
      {
        Task t5 = Task.Factory.StartNew(P, TaskCreationOptions.AttachedToParent);
        t5.Wait();
        R(o);
      }
      static void P()
      {
        Console.WriteLine("t5 runs " + Task.CurrentId.ToString());
        Q();
      }
      static void Q()
      {
        // breaks here at the same time as N and M
        Interlocked.Increment(ref cc);
        while (cc < 4)
        {
          ;
        }
        // task 5 dies here freeing task 4 (its parent)
        Console.WriteLine("t5 dies " + Task.CurrentId.ToString());
        waitFor5 = false;
      }
      static void R(object o)
      {
        if ((int)o == 2)
        {
          //wait for task5 to die
          while (waitFor5) { ;}
    
    
          int i;
          //spin up all procs
          for (i = 0; i < pcount - 4; i++)
          {
            Task t = Task.Factory.StartNew(() => { while (true);});
            Console.WriteLine("Started task " + t.Id.ToString());
          }
    
          Task.Factory.StartNew(T, i + 1 + 5, TaskCreationOptions.AttachedToParent); //scheduled
          Task.Factory.StartNew(T, i + 2 + 5, TaskCreationOptions.AttachedToParent); //scheduled
          Task.Factory.StartNew(T, i + 3 + 5, TaskCreationOptions.AttachedToParent); //scheduled
          Task.Factory.StartNew(T, i + 4 + 5, TaskCreationOptions.AttachedToParent); //scheduled
          Task.Factory.StartNew(T, (i + 5 + 5).ToString(), TaskCreationOptions.AttachedToParent); //scheduled
    
          //BP4 - 1 in M, 2 in R, 3 in J, 4 in R, 5 died
          Debugger.Break();
        }
        else
        {
          Debug.Assert((int)o == 4);
          t3.Wait();
        }
      }
      static void T(object o)
      {
        Console.WriteLine("Scheduled run " + Task.CurrentId.ToString());
      }
      static Task t1, t2, t3, t4;
      static int aa = 0;
      static int bb = 0;
      static int cc = 0;
      static bool waitFor1 = true;
      static bool waitFor5 = true;
      static int pcount;
      static S mylock = new S();
    }
    

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

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

  2. 選取 [建置] 功能表中的 [重建方案]

請注意,對 Debugger.Break (在 C++ 範例中為 DebugBreak) 的呼叫有四個。 因此,您不需要插入中斷點。 僅只執行應用程式會使偵錯工具中斷最多四次。

使用平行堆疊視窗:執行緒檢視

若要開始,請在 [偵錯] 功能表中,選取 [開始偵錯]。 等到碰上第一個中斷點為止。

檢視單一執行緒的呼叫堆疊

  1. 在 [偵錯] 功能表上,指向 [視窗],然後選取 [執行緒]。 將 [執行緒] 視窗停駐在 Visual Studio 底部。

  2. 在 [偵錯] 功能表中,指向 [視窗],然後選取 [呼叫堆疊]。 將 [呼叫堆疊] 視窗停駐在 Visual Studio 底部。

  3. 按兩下 [執行緒] 視窗中的執行緒,使它成為目前執行緒。 目前執行緒具有黃色箭號。 當您變更目前執行緒時,其呼叫堆疊會出現在 [呼叫堆疊] 視窗中。

檢查平行堆疊視窗

在 [偵錯] 功能表上,指向 [視窗],然後選取 [平行堆疊]。 確定左上角的方塊中已選取 [執行緒]

透過使用 [平行堆疊] 視窗,您可以在一個檢視中同時檢視多個呼叫堆疊。 下圖顯示 [呼叫堆疊] 視窗上方的 [平行堆疊] 視窗。

Screenshot of Threads view in Parallel Stacks window.

Threads view in Parallel Stacks window

主執行緒的呼叫堆疊會出現在一個方塊中,而其他四個執行緒的呼叫堆疊會一起出現在另一個方塊中。 四個執行緒形成一組是因為它們的堆疊框架共用相同的方法內容,也就是說,它們位於相同的方法中:ABC。 若要檢視共用同一方塊的執行緒識別碼和名稱,請暫留在具有標頭 ([#] 執行緒) 的方塊上。 目前執行緒以粗體顯示。

Screenshot of Tooltip that shows thread IDs and names.

Tooltip that shows thread IDs and names

黃色箭號表示目前執行緒的作用中堆疊框架。

您可以在 [呼叫堆疊] 視窗中按一下滑鼠右鍵,以設定堆疊框架要顯示多少詳細資料 ([模組名稱]、[參數類型]、[參數名稱]、[參數值]、[行號] 和 [位元組位移])

方塊周圍的藍色醒目提示表示目前執行緒是該方塊的一部分。 工具提示中也以粗體堆疊框架來表示目前執行緒。 如果按兩下 [執行緒] 視窗中的 [主要執行緒],可以觀察到 [平行堆疊] 視窗中醒目提示的箭頭會相應移動。

Screenshot of Highlighted main thread in Parallel Stacks window.

Highlighted main thread in Parallel Stacks window

繼續執行直到第二個中斷點為止

若要繼續執行直到碰上第二個中斷點為止,請選取 [偵錯] 功能表的 [繼續]。 下圖顯示第二個中斷點上的執行緒樹狀結構。

Screenshot of Parallel Stacks window that shows many branches.

Parallel Stacks window that shows many branches

在第一個中斷點上,四個執行緒都是從 S.A 跳至 S.B,再跳至 S.C 方法。 這項資訊在 [平行堆疊] 視窗中仍然可見,但四個執行緒已更往前執行。 其中一個繼續執行至 S.D,再執行至 S.E。另一個繼續執行至 S.F、S.G 和 S.H。其他兩個繼續執行至 S.I 和 S.J,而在這裡,其中一個跳至 S.K,另一個繼續執行至非使用者外部程式碼。

您可以將滑鼠游標停留於堆疊框架上,以查看執行緒 ID 和其他框架詳細資料。 藍色醒目提示表示目前執行緒,黃色箭號表示目前執行緒的作用中堆疊框架。

您可以將滑鼠游標停留於方塊標題上,例如 [1 個執行緒] 或 [2 個執行緒],以查看執行緒的執行緒識別碼。 您可以將滑鼠游標停留於堆疊框架上,以查看執行緒 ID 和其他框架詳細資料。 藍色醒目提示表示目前執行緒,黃色箭號表示目前執行緒的作用中堆疊框架。

布條圖示 (交織線) 表示非目前執行緒的使用中堆疊框架。 在 [呼叫堆疊] 視窗中,按兩下 S.B 來切換框架。 [平行堆疊] 視窗使用弧形箭號圖示來表示目前執行緒的目前堆疊框架。

注意

有關 [平行堆疊] 視窗中所有圖示的描述,請參閲使用 [平行堆疊] 視窗

在 [執行緒] 視窗中,切換執行緒並觀察 [平行堆疊] 視窗中的檢視已更新。

您可以使用 [平行堆疊] 視窗中的捷徑功能表,切換至另一個執行緒,或切換至另一個執行緒的另一個框架。 例如,以滑鼠右鍵按一下 S.J,指向 [切換至框架],然後選取命令。

Screenshot of Parallel Stacks Path of Execution.

Parallel Stacks Path of Execution

以滑鼠右鍵按一下 S.C,並指向 [切換至框架]。 其中一個命令有核取記號,表示目前執行緒的堆疊框架。 您可以切換至相同執行緒的該框架 (只有弧形箭號會移動),也可以切換至另一個執行緒 (藍色醒目提示也會移動)。 下圖顯示子功能表。

Screenshot of Stacks menu with 2 options on C while J is current.

Stacks menu with 2 options on C while J is current

當方法內容只有與一個堆疊框架建立關聯時,方塊標題會顯示 [1 個執行緒],您只要按兩下就可以切換至該框架。 如果您按兩下的方法內容有 1 個以上關聯的框架,則會自動出現功能表。 隨著您將滑鼠游標停留於方法內容上,請注意右邊的黑色三角形。 按一下該三角形也會顯示捷徑功能表。

對於具有許多執行緒的大型應用程式,您可能會想要只專注於其中一部分執行緒。 [平行堆疊] 視窗可以只顯示加上旗標之執行緒的呼叫堆疊。 若要將執行緒加上旗標,請使用捷徑功能表或執行緒的第一個儲存格。

在工具列上,選取清單方塊旁邊的 [僅顯示有旗標的項目] 按鈕。

Screenshot of Parallel Stacks window and tooltip.

Parallel Stacks window and tooltip

現在,只有已標幟的執行緒才會顯示在 [平行堆疊] 視窗中。

繼續執行直到第三個中斷點為止

  1. 若要繼續執行直到碰上第三個中斷點為止,請在 [偵錯] 功能表上,選取 [繼續]

    當多個執行緒在相同方法中但方法不在呼叫堆疊的開頭時,方法會出現在不同方塊中。 位於目前中斷點的例子有 S.L,其中有三個執行緒,且分別出現在三個方塊中。 按兩下 S.L。

    Screenshot of Execution path in Parallel Stacks window.

    Execution path in Parallel Stacks window

    請注意,S.L 在其他兩個方塊中是粗體,所以您可以看到它出現在其他地方。 如果您要查看有哪些框架呼叫 S.L 和它呼叫哪些框架,請選取工具列的 [切換方法檢視] 按鈕。 下圖顯示 [平行堆疊] 視窗的方法檢視。

    Screenshot of Method view in Parallel Stacks window.

    Method view in Parallel Stacks window

    請注意圖表如何隨選取的方法而轉移,以及它在檢視中間如何放在自己的方塊中。 被呼叫者和呼叫者分別顯示在頂端和底端。 再次選取 [切換方法檢視] 按鈕以結束這個模式。

    [平行堆疊] 視窗的捷徑功能表還有下列其他項目。

    • [十六進位顯示] 在十進位和十六進位之間切換工具提示中的數字。

    • 符號設定開啟相應的對話方塊。

    • 在原始程式碼中顯示執行緒切換原始程式碼中執行緒標記的顯示,它顯示原始程式碼中的執行緒位置。

    • [顯示外部程式碼] 會顯示所有框架,即使不在使用者程式碼中也一樣。 請試著使用它來查看圖表如何展開來容納其他框架 (這些框架可能會因為您沒有它們的符號而呈現暗灰色)。

  2. 在 [平行堆疊] 視窗中,確定工具列上的 [自動捲動到目前堆疊框架] 按鈕已啟用。

    當您具有大型圖表並逐步執行至下一個中斷點時,您可能會想要讓檢視自動捲動至目前執行緒的作用中堆疊框架,也就是最先碰上中斷點的執行緒。

  3. 繼續之前,在 [平行堆疊] 視窗中一直捲動到最左邊和最下方。

繼續執行直到第四個中斷點為止

  1. 若要繼續執行直到碰上第四個中斷點為止,請選取 [偵錯] 功能表的 [繼續]

    請注意檢視如何自動捲動至定位。 在 [執行緒] 視窗中切換執行緒,或在 [呼叫堆疊] 視窗中切換堆疊框架,並注意檢視如何一律自動捲動至正確的框架。 關閉 [自動捲動到目前工具框架] 選項並檢視差異。

    [概觀] 也有助於在 [平行堆疊] 視窗中顯示大型圖表。 根據預設,[鳥瞰檢視] 處於啟用狀態。 但是,您可以透過按一下視窗右下角捲軸之間的按鈕來切換它,如下圖所示。

    Screenshot of Birds eye view in Parallel Stacks window.

    Bird's-eye view in Parallel Stacks window

    在鳥瞰檢視中,您可以移動矩形以快速移動瀏覽圖表。

    另一種往任何方向移動圖表的方式是選取圖表的空白區域,並拖曳至您要的位置。

    若要放大和縮小圖表,請在移動滑鼠滾輪時按住 CTRL。 或者,選取工具列的 [縮放] 按鈕,然後使用 [縮放] 工具。

    您也可以按一下 [工具] 功能表,再按一下 [選項],然後選取或清除 [偵錯] 節點下的選項,以使用由上而下的方向來檢視堆疊,而非由下而上。

  2. 繼續之前,選取 [偵錯] 功能表上的 [停止偵錯] 以結束執行。

使用平行工作視窗和平行堆疊視窗的工作檢視

繼續之前,我們建議您完成先前的程序。

重新啟動應用程式直到碰上第一個中斷點為止:

  1. 選取 [偵錯] 功能表上的 [開始偵錯],並等待碰上第一個中斷點。

  2. 在 [偵錯] 功能表上,指向 [視窗],然後選取 [執行緒]。 將 [執行緒] 視窗停駐在 Visual Studio 底部。

  3. 在 [偵錯] 功能表中,指向 [視窗],然後選取 [呼叫堆疊]。 將 [呼叫堆疊] 視窗固定在 Visual Studio 底部。

  4. 按兩下 [執行緒] 視窗中的執行緒,使它成為目前執行緒。 目前執行緒具有黃色箭號。 當您變更目前執行緒時,其他視窗會隨之更新。 接下來,我們會檢查工作。

  5. 在 [偵錯] 功能表上,指向 [視窗],然後選取 [工作]。 下圖顯示 [工作] 視窗。

    Screenshot of Four running tasks in Tasks window.

    Four running tasks in Tasks window

    對於每一個執行中的工作,您可以讀取其 ID (由名稱相同的屬性傳回)、執行這個工作之執行緒的 ID 和名稱,以及它的位置 (將滑鼠游標停留於工作上會顯示包含整個呼叫堆疊的工具提示)。 另外,在 [工作] 資料行下,您可以查看傳入工作中的方法,也就是起點。

    您可以排序任何資料行。 請注意表示排序資料行和方向的排序圖像。 您也可以將資料行向左或向右拖曳,以重新排列資料行。

    黃色箭號表示目前工作。 您可以按兩下工作或使用捷徑功能表來切換工作。 當您切換工作時,基礎執行緒會變成目前執行緒,而其他視窗也會隨之更新。

    手動從一個工作切換到另一個工作時,箭頭外框指示非目前工作的目前偵錯工具內容。

    當您手動在兩個工作之間切換時,黃色箭號會移動,但白色箭號仍然會顯示造成偵錯工具中斷的工作。

繼續執行直到第二個中斷點為止

若要繼續執行直到碰上第二個中斷點為止,請選取 [偵錯] 功能表的 [繼續]

以前,[狀態] 資料行將所有工作顯示為 [使用中],但現在有兩個工作為 [已封鎖]。 工作可能會因為許多不同的原因而受阻。 在 [狀態] 資料行中,將滑鼠游標停留於等待中工作上,以了解受阻的原因。 例如,在下圖中,工作 11 正在等待工作 12。

Screenshot of Two waiting tasks in Tasks window.

以前,[狀態] 資料行將所有工作顯示為 [使用中],但現在有兩個工作為 [已封鎖]。 工作可能會因為許多不同的原因而受阻。 在 [狀態] 資料行中,將滑鼠游標停留於等待中工作上,以了解受阻的原因。 例如,在下圖中,工作 4 正在等待工作 5。

Two waiting tasks in Tasks window

工作 4 又在等待指派給工作 2 的執行緒所擁有的監視器。 (以滑鼠右鍵按一下標頭資料列,然後選擇 [資料行]>[執行緒指派] 以檢視工作 2 的執行緒指派值)。

Waiting task and tooltip in Tasks window

您可以按一下 [工作] 視窗的第一個資料行中的旗標,將工作加上旗標。

您可以使用旗標,在相同偵錯工作階段中的不同中斷點之間追蹤工作,或篩選在 [平行堆疊] 視窗中出現呼叫堆疊的工作。

您先前在使用 [平行堆疊] 視窗時,已檢視應用程式執行緒。 再次檢閱 [平行堆疊] 視窗,但這次檢閱應用程式工作。 做法是在左上方的方塊中選取 [工作]。 下圖顯示 [工作檢視]。

Screenshot of Tasks view in Parallel Stacks window.

Tasks view in Parallel Stacks window

目前未執行工作之執行緒不會出現在 [平行堆疊] 視窗的 [工作檢閱] 中。 另外,對於在執行工作的執行緒,某些與工作無關的堆疊框架則會從堆疊的上方和下方被過濾掉。

再次檢閲 [工作] 視窗。 以滑鼠右鍵按一下任何資料行標頭,以查看資料行的捷徑功能表。

您可以使用捷徑功能表來加入或移除資料行。 例如,AppDomain 資料行未選取,所以不會出現在清單中。 選取 [父代]。 這四項工作在 [父代] 資料行中沒有顯示任何值。

繼續執行直到第三個中斷點為止

若要繼續執行直到碰上第三個中斷點為止,請在 [偵錯] 功能表上,選取 [繼續]

Screenshot of Parent-child view in Tasks window.

在此範例執行中,請注意工作 11 和工作 12 在同一個執行緒上執行 (如果 [執行緒指派] 資料行處於隱藏狀態,則顯示它)。 此資訊不會顯示在 [執行緒] 視窗中; 在這裡看到它是 [工作] 視窗的另一個權益。 若要確認這一點,請檢視 [平行堆疊] 視窗。 確定您檢閱的是 [工作]。 您可以瀏覽 [平行堆疊] 視窗上的工具提示來尋找工作 11 和 12。

Task view in Parallel Stacks window

新工作 (工作 5) 現在正在執行,而工作 4 現在正在等待。 您可以在 [狀態] 視窗中將滑鼠游標停留於等待中工作上,以查看原因。 在 [父代] 資料行中,請注意工作 4 是工作 5 的父代。

若要更明確顯示父子式關聯性,請以滑鼠右鍵按一下資料行標頭,然後選取 [父子式檢視]。 您應該會看到下圖。

Parent-child view in Tasks window

請注意工作 4 和工作 5 在同一個執行緒上執行 (如果 [執行緒指派] 資料行處於隱藏狀態,則顯示它)。 此資訊不會顯示在 [執行緒] 視窗中; 在這裡看到它是 [工作] 視窗的另一個權益。 若要確認這一點,請檢視 [平行堆疊] 視窗。 確定您檢閱的是 [工作]。 在 [工作] 視窗中按兩下工作 4 和工作 5,找出它們。 這樣做時,[平行堆疊] 視窗中的藍色醒目提示會隨之更新。 您也可以瀏覽 [平行堆疊] 視窗上的工具提示來尋找工作 4 和 5。

Task view in Parallel Stacks window

在 [平行堆疊] 視窗中,以滑鼠右鍵按一下 S.P,然後選取 [移至執行緒]。 視窗會切換至 [執行緒檢視],且檢視中會有對應的框架。 您可以在相同執行緒上同時查看這兩項工作。

Highlighted thread in threads view

相較於 [執行緒] 視窗,這是 [平行堆疊] 視窗之 [工作檢閱] 的另一項優點。

繼續執行直到第四個中斷點為止

若要繼續執行直到碰上第三個中斷點為止,請在 [偵錯] 功能表上,選取 [繼續]。 選取 [識別碼] 資料行標頭,依識別碼排序。 您應該會看到下圖。

Screenshot of Four task states in Parallel Stacks window.

工作 10 和工作 11 現在相互等待並且已封鎖。 現在還排程了幾個新工作。 排程工作是指已在程式碼中啟動但尚未執行的工作。 因此,它們的 [位置][執行緒指派] 資料行顯示預設訊息或為空。

Four task states in Parallel Stacks window

因為工作 5 已完成,所以不會再出現。 如果您的電腦上不是這樣,也沒有顯示死結,請按 F11 逐步執行一次。

工作 3 和工作 4 現在相互等待並且已封鎖。 工作 2 還有 5 個新的子工作已經進入排程準備執行。 排程工作是指已在程式碼中啟動但尚未執行的工作。 因此,其 [位置] 和 [執行緒指派] 資料行都是空的。

再次檢視 [平行堆疊] 視窗。 每一個方塊的標題都有工具提示會顯示執行緒 ID 和名稱。 切換至 [平行堆疊] 視窗中的 [工作檢閱]。 將滑鼠游標停留於標題上,以查看工作 ID 和名稱,以及工作的狀態,如下圖所示。

Header tooltip in Parallel Stacks window

您可以依資料行將工作分組。 在 [工作] 視窗中,以滑鼠右鍵按一下 [狀態] 資料行標頭,然後選取 [依狀態群組]。 下圖顯示依狀態分組的 [工作] 視窗。

Screenshot of Grouped tasks in Tasks window.

Grouped tasks in Tasks window

您也可以依其他任何資料行進行分組。 將工作分組可讓您專注於一部分工作。 每一個可摺疊的群組都有一些組成該群組的項目。

[工作] 視窗中最後一項要說明的功能,就是您以滑鼠右鍵按一下工作時所顯示的捷徑功能表。

視工作的狀態而定,捷徑功能表會顯示不同的命令。 命令可能包括 [複製]、[全選]、[十六進位顯示]、[切換至工作]、[凍結指派的執行緒]、[凍結這個執行緒以外的所有執行緒]、[解除凍結指派的執行緒] 和 [加上旗標]

您可以凍結一項或多項工作的基礎執行緒,也可以凍結指派的執行緒除外的所有執行緒。 凍結的執行緒在 [工作] 視窗中以藍色「暫停」圖示表示,就像在 [執行緒] 視窗中一樣。

摘要

本逐步解說示範 [平行工作] 和 [平行堆疊] 偵錯工具視窗。 請在使用多執行緒程式碼的實際專案上使用這些視窗。 您可以檢查以 C++、C# 或 Visual Basic 撰寫的平行程式碼。