併發程式設計-10.使用 Visual Studio 除錯多執行緒應用程式

F(x)_King發表於2024-03-30

引入多執行緒除錯

除錯是每個 .NET 開發人員技能的關鍵組成部分。 沒有人會編寫沒有錯誤的程式碼,將多執行緒結構引入到您的專案中只會增加引入的機會
錯誤。 由於 .NET 和 C# 新增了更多對並行程式設計和併發的支援,Visual Studio 新增了更多除錯功能來支援這些構造。

如今,Visual Studio 為現代 .NET 開發人員提供了以下多執行緒除錯功能:

  • 執行緒(Threads):此視窗顯示應用程式在除錯時使用的執行緒列表。 它還指示哪個執行緒在程式碼中的斷點處停止時處於活動狀態。
  • 並行堆疊(Parallel Stacks):此視窗允許開發人員在單個檢視中視覺化應用程式中每個執行緒的呼叫堆疊。 在視窗中選擇一個執行緒將在“呼叫堆疊”視窗中顯示所選執行緒的呼叫堆疊資訊。
  • 並行監視:此視窗的工作方式類似於“監視”視窗,不同之處在於您可以檢視應用程式中每個活動執行緒上的監視表示式的值。
  • 除錯位置:此工具欄允許您在除錯多執行緒應用程式時縮小焦點。 它具有用於選擇程序、執行緒和堆疊幀的欄位。 工具欄上還有一些按鈕,以便您可以標記和取消標記要監視的執行緒。
  • 任務:此視窗顯示應用程式中每個正在執行的任務,並提供有關正在執行該任務的執行緒、任務狀態及其呼叫堆疊的資訊。 您還可以檢視每個任務的起點(傳遞給要執行的任務的方法或委託)。
  • 附加到程序:此視窗允許您將 Visual Studio 偵錯程式附加到本地計算機或遠端計算機上的程序。 使用多執行緒 UI 應用程式時,遠端除錯非常有用。 它允許開發人員在處理器核心數量與機器上不同的系統上除錯應用程式。 它們還可以附加到在執行生產環境中存在的其他程序的系統上執行的遠端程序。
  • GPU 執行緒:此視窗顯示有關 GPU 上執行的執行緒的資訊。 這用於 C++ 應用程式,超出了本書的範圍。

在前面的章節中,我們將使用這些除錯工具單步除錯本書前面一些章節中的專案中的多執行緒程式碼。 讓我們首先了解“執行緒”和“附加到程序”視窗以及“除錯位置”工具欄。

除錯執行緒和程序

在本節中,我們將除錯第 1 章中的 BackgroundPingConsoleApp。您可以使用第 1 章中已完成的專案。 我們將除錯應用程式並發現“除錯位置”工具欄和“執行緒”視窗的一些功能。

除錯具有多個執行緒的專案

我們將要處理的專案是一個簡單的專案,它建立一個後臺執行緒來檢查網路是否可用。

讓我們開始除錯示例:

  1. 首先在 Visual Studio 中開啟 BackgroundPingConsoleApp,然後在 C# 編輯器中開啟 Program.cs。

  2. 在 while 迴圈內的 Thread.Sleep(100) 語句上設定斷點。

  3. 選擇檢視 | 工具欄| 除錯位置顯示“除錯位置”工具欄:

圖 10.1 – Visual Studio 中的除錯位置工具欄

image

當我們開始除錯時,我們將使用這個工具欄。 當 Visual Studio 中沒有活動的除錯會話時,所有欄位都會被禁用。

  1. 開始除錯專案。 當 Visual Studio 在斷點處中斷時,請注意“除錯位置”工具欄的狀態:

圖 10.2 – 使用“除錯位置”工具欄進行除錯

image

工具欄提供了幾個下拉控制元件來選擇範圍內的程序、執行緒和堆疊幀屬性。 除非您使用“附加到程序”視窗顯式除錯多個程序,否則“程序”下拉選單將僅包含單個程序。 您還可以在 Visual Studio 中設定多個啟動專案來實現此目的。

執行緒下拉選單包含屬於所選程序的所有執行緒。 該控制元件中選定的執行緒是我們建立的後臺執行緒,因為斷點是新增在該後臺執行緒執行的程式碼中的。

堆疊幀下拉選單包含當前執行緒呼叫堆疊中的幀列表。

執行緒下拉選單右側有一個“切換當前執行緒標記狀態”按鈕。 我們稍後將在“切換和標記執行緒”部分中瞭解如何標記執行緒。

  1. 接下來,選擇除錯 | 窗戶 | 執行緒開啟“執行緒”視窗:

圖 10.3 – 在“執行緒”視窗處於活動狀態時進行除錯

image

預設情況下,“執行緒”視窗將在左下皮膚中開啟,其中包含“輸出”、“區域性變數”和“監視”除錯視窗。

  1. 最後,展開“Threads”視窗,以便我們可以探索和討論其功能:

圖 10.4 – 仔細觀察“執行緒”視窗

image

探索執行緒視窗

執行緒視窗在一個小視窗中提供了大量有用的資訊。 我們將首先討論為列表中每個執行緒顯示的資料:

程序 ID:預設情況下,執行緒列表按程序 ID 分組。 此分組可以透過視窗工具欄中的分組依據下拉選單進行控制。 程序 ID 分組還顯示其組中的執行緒數。 這在處理大量執行緒時非常有用。

  • ID:這是列表中每個執行緒的 ID
  • 託管ID:這是每個執行緒的Thread.ManagedThreadId屬性
  • 類別(Category):描述執行緒的型別(主執行緒、工作執行緒等)
  • 名稱(Name):該欄位包含每個執行緒的 Thread.Name 屬性。 如果執行緒沒有名稱,則此欄位中將顯示
  • 位置(Location):該欄位包含其呼叫堆疊中每個執行緒的當前堆疊幀。 您可以單擊此欄位中的下拉選單以顯示執行緒的完整呼叫堆疊。
  • 預設情況下,一些附加欄位是隱藏的。 您可以透過選擇“執行緒”視窗工具欄中的“列”按鈕來隱藏或顯示列。 選擇或取消選擇您想要顯示或隱藏的列。 這些是最初隱藏的列:
  • 優先順序(Priority):顯示系統分配給執行緒的優先順序
  • 關聯掩碼(Affinity Mask):關聯掩碼確定執行緒可以在哪些處理器上執行。 這是系統決定的
  • 程序名稱(Process Name:):這是執行緒所屬程序的名稱
  • 程序ID(Process ID):這是執行緒所屬程序的ID
  • 傳輸限定符(Transport Qualifier):這標識連線到偵錯程式的機器。 這對於遠端除錯很有用

現在,讓我們回顧一下“執行緒”視窗中可用的工具欄專案:

• 搜尋:這允許您搜尋話題。 如果您希望搜尋結果包含所有呼叫堆疊資訊,您可以開啟“在搜尋中包含呼叫堆疊”按鈕
• 標記:使用此下拉按鈕,您可以選擇僅標記我的程式碼或標記自定義模組選擇
• 分組依據:此下拉選單允許您按不同欄位對話題進行分組。 預設情況下,它們按程序 ID 分組
• 列:這將開啟“列”選擇視窗,以便您可以自定義“執行緒”視窗中顯示的列
• 展開/摺疊呼叫堆疊:這兩個按鈕在“位置”列中展開或摺疊呼叫堆疊
• 展開/摺疊組:這兩個按鈕可展開或摺疊執行緒分組
• 凍結執行緒:這會凍結視窗中所有選定的執行緒
• 解凍執行緒:這會解凍視窗中所有選定的執行緒

讓我們嘗試一下搜尋功能。 開始除錯BackgroundPingConsoleApp 專案。 當它到達斷點時,在搜尋欄位中搜尋 Anon 以查詢其呼叫堆疊包含我們的匿名方法的執行緒:

圖 10.5 – 在“執行緒”視窗中搜尋

image

Threads 視窗現在應該只包含工作執行緒的行,AnonymousMethod 的 Anon 部分以黃色突出顯示。

切換和標記執行緒

除錯多執行緒應用程式時,“執行緒”視窗提供了強大的功能。 我們在上一節中談到了其中一些功能。 在本節中,我們將學習如何切換執行緒、標記執行緒以及凍結或解凍執行緒。 讓我們從在 BackgroundPingConsoleApp 專案中的執行緒之間切換開始。

切換執行緒

您可以使用“執行緒”視窗中的上下文選單將上下文切換到不同的執行緒。 執行專案並等待偵錯程式在我們的匿名方法的斷點處暫停。 當偵錯程式中暫停執行時,右鍵單擊“主執行緒”行並選擇“切換到執行緒”。偵錯程式中的游標應切換到 Console.ReadLine() 語句。這是主執行緒等待 使用者在控制檯中按任意鍵:

圖 10.6 – 在 Visual Studio 偵錯程式中切換執行緒

image

您可以看到,在除錯具有六個或更多活動執行緒的並行操作時,此函式非常有用。 接下來,我們將學習如何使用“標記執行緒”功能來關注特定執行緒。

標記執行緒

在本節中,您將瞭解如何在“執行緒”視窗中除錯時縮小視野。 透過僅標記我們關心的執行緒,我們可以減少視窗中的混亂。以下是標記執行緒的方法:

  1. 如果您尚未除錯BackgroundPingConsoleApp 專案,請立即開始除錯並等待其在斷點處停止。
  2. 當偵錯程式在應用程式中暫停時,右鍵單擊“主執行緒”行並選擇“標記”。 該行中的標誌圖示現在應為橙色。
  3. 對呼叫堆疊中包含帶有 AnonymousMethod 的 Worker Thread 的行執行相同操作
  4. 接下來,單擊視窗工具欄中的“僅顯示標記的執行緒”按鈕:

圖 10.7 – 僅在“執行緒”視窗中顯示標記的執行緒

image

這使得僅跟蹤對當前除錯會話重要的執行緒變得更簡單。您可以再次單擊該按鈕以關閉該按鈕並檢視所有執行緒。 還可以在“並行監視”和“並行堆疊”視窗中標記執行緒。 它們的標記狀態將在所有這些視窗和除錯位置工具欄上持續存在。

有一種更簡單的方法可以在我們的應用程式中標記這兩個執行緒。 這是我們應用程式程式碼中僅有的兩個執行緒。 因此,我們可以使用工具欄中的“僅標記我的程式碼”按鈕來標記它們。

  1. 取消選擇“僅顯示標記的執行緒”工具欄按鈕
  2. 右鍵單擊視窗中標記的行之一,然後選擇全部取消標記
  3. 現在,單擊工具欄中的“僅標記我的程式碼”。 相同的兩個執行緒將再次被標記:

圖 10.8 – 僅標記屬於我們程式碼的執行緒

image

這比在列表中一一選擇執行緒要容易得多。 哪些執行緒是我們程式碼的一部分可能並不總是那麼明顯。 在下一節中,我們將學習如何凍結執行緒。

凍結執行緒

在“執行緒”視窗中凍結或解凍執行緒相當於呼叫 SuspendThread 或 ResumeThread Windows 函式。 如果凍結的執行緒尚未執行任何程式碼,則除非解凍,否則它將永遠不會啟動。 如果執行緒當前正在執行,則當在 Visual Studio 中呼叫 Freeze 執行緒時,該執行緒將會暫停。

讓我們嘗試凍結和解凍 BackgroundPingConsoleApp 專案中的工作執行緒,看看偵錯程式中會發生什麼:

  1. 在執行應用程式之前,在 while (true) 和 Console 處新增新斷點。 ReadKey() 語句。 將現有斷點保留在 Thread.Sleep(100)
  2. 開始除錯應用程式
  3. 當偵錯程式在 while (true) 行中斷時,右鍵單擊包含 AnonymousMethod 的工作執行緒並選擇 Freeze.
  4. 繼續除錯; 它應該在 Console.ReadKey() 行而不是 Thread.Sleep(100) 處中斷。 這是因為工作執行緒當前沒有執行:

圖 10.9 – 在“執行緒”視窗中凍結工作執行緒

image

  1. 再次右鍵單擊工作執行緒並選擇解凍
  2. 現在,再次繼續除錯。 Visual Studio 在匿名方法內的 Thread.Sleep(100) 行處中斷。

這顯示了“執行緒”視窗的功能在除錯多執行緒應用程式時非常有用。

現在我們已經瞭解瞭如何透過使用“執行緒”視窗切換、凍結和標記執行緒來除錯多執行緒應用程式,接下來我們來了解如何在除錯時利用並行堆疊和並行監視視窗等附加功能。

除錯並行應用程式

Visual Studio 提供了多個用於並行除錯的視窗。 雖然“執行緒”視窗適用於任何型別的多執行緒應用程式,但其他視窗在處理應用程式中的“任務”物件時提供了附加功能和檢視。

我們將從“並行堆疊”視窗開始瞭解這些功能。

使用並行堆疊視窗

“並行堆疊”視窗提供應用程式中執行緒或任務的視覺化表示。這是視窗中的兩個不同檢視。 您可以透過在檢視下拉框中選擇執行緒或任務來在它們之間切換。 以下螢幕截圖顯示了除錯BackgroundPingConsoleApp 專案時的執行緒檢視示例:

圖 10.10 – 在“執行緒”檢視中檢視“並行堆疊”視窗

image

“並行堆疊”視窗包含一個工具欄,其中從左到右包含以下專案。 您可以透過檢查 Visual Studio 視窗中工具欄項的工具提示來進行操作:

  • 搜尋:這允許使用與“執行緒”視窗中可用的相同型別的搜尋功能。搜尋欄位右側有“查詢上一個”和“查詢下一個”按鈕。

  • 檢視:此下拉選單在“執行緒”和“任務”檢視之間切換

  • 僅顯示標記:此切換將隱藏任何未標記的執行緒

  • 切換方法檢視:這將切換到當前所選方法及其呼叫堆疊的檢視

  • 自動滾動到當前堆疊幀:這將在單步執行偵錯程式時將當前堆疊幀滾動到圖中的檢視中。 預設情況下此選項處於開啟狀態。

  • 切換縮放控制元件:這會隱藏或顯示圖表表面上的縮放控制元件。 該選項預設開啟。

  • 反向佈局:此選項映象當前檢視的佈局

  • 儲存圖表:此選項將當前圖表儲存到.png 檔案中

要檢查視窗的任務檢視,我們需要開啟一個包含一些任務物件的不同專案。 讓我們透過開啟本書前一章中的專案來使用“任務”檢視:

  1. 開啟第 5 章中的 TaskSamples 專案

  2. 開啟 Examples.cs 並在 ProcessOrders 方法的第一行設定斷點。

  3. 開始除錯。 當偵錯程式在斷點處停止時,選擇 除錯| 窗戶 | 並行堆疊。

  4. 切換到並行堆疊視窗中的任務檢視:

圖 10.11 – 任務檢視中的並行堆疊視窗

image

尚未開始任何任務,因此這裡沒什麼可看的。 有一個非同步邏輯堆疊塊看起來已經準備好開始分析一些非同步工作。

  1. 在 Tasks.WaitAll 語句上新增斷點,然後單擊繼續

  2. 現在,再次檢查並行堆疊視窗:

圖 10.12 – 任務處於活動狀態時的並行堆疊視窗

image

在這種情況下,並行堆疊視窗捕獲了一個正在執行的任務的執行情況和另一個準備執行的任務的執行情況。 這個任務檢視和我們在本章中所做的一些執行緒分析之間存在一些差異:

  • 任務檢視中僅顯示正在執行的任務
  • 任務檢視的堆疊嘗試僅顯示相關的呼叫堆疊資訊。 如果堆疊框架不相關,則可以從頂部和底部修剪它們。 如果您需要檢視整個呼叫堆疊,請切換回“執行緒”檢視。
  • 任務檢視中將為每個活動任務顯示一個單獨的塊,即使它們被分配給同一執行緒也是如此。

您可以將滑鼠懸停在任務呼叫堆疊中的一行上以檢視有關其執行緒和堆疊幀的更多資訊:

圖 12.13 – 檢視有關呼叫堆疊幀的更多資訊

image

如果要將“任務”檢視旋轉到特定方法,可以使用“切換方法檢視”按鈕:

  1. 在 TaskSamples 專案中啟動新的除錯會話
  2. 在PrepareOrders方法中的退貨訂單語句上設定新的斷點
  3. 單擊繼續。 當偵錯程式在PrepareOrders 方法中中斷時,並行監視視窗將顯示活動任務。
  4. 單擊“切換方法檢視”按鈕。 您現在擁有以方法為中心的“任務”檢視檢視,並且可以將滑鼠懸停在“PrepareOrders”方法上以獲取更多呼叫堆疊和執行緒資訊:

圖 10.14 – 利用並行堆疊視窗的方法檢視區域

image

使用並行監視視窗

並行監視視窗與 Visual Studio 中的監視視窗類似,但它顯示有關跨執行緒的監視表示式值的附加資訊,並可訪問表示式中的資料。

在此示例中,我們將修改 TaskSamples 專案中的 Examples 類以新增可供多個執行緒使用的狀態:

  1. 首先向 Examples 類新增一個私有變數:
private List<Order> _sharedOrders;
  1. 在 ProcessOrders 中新增一行,將訂單分配給 _sharedOrders:
private List<Order> PrepareOrders(List<Order> orders)
{
    // TODO: Prepare orders here
    _sharedOrders = orders;
    return orders;
}
  1. 保留上一個示例中的斷點並開始除錯。 繼續,直到偵錯程式在 ProcessOrders 內的返回訂單語句處中斷。

  2. 選擇除錯 | 窗戶 | 並行監視 1 開啟“並行監視 1”視窗。 您最多可以開啟四個並行觀察視窗來分隔您觀察的表情。

  3. 在 Parallel Watch 1 視窗中,您將在上下文中看到當前執行緒的一行。 將監視新增到 _sharedOrders 私有變數:

圖 10.15 – 在 Parallel Watch 1 視窗中新增監視表示式

image

該視窗指示任務 6 在範圍內具有 _sharedOrders,並且變數中的訂單計數為 0。

  1. 右鍵單擊“執行緒”視窗中的“主執行緒”,然後選擇“切換到執行緒”。 在Parallel Watch 1視窗中,任務不再在範圍內,因此標題標籤已從Task更改為Thread,並且將顯示Main Thread的ID屬性:

圖 10.16 – 在主執行緒上檢視監視的變數

image

  1. 最後,選擇除錯 | 窗戶 | 任務開啟任務視窗:

圖 10.17 – 除錯時檢視任務視窗

image

“任務”視窗將顯示有關除錯會話範圍內的任務的資訊。 視窗中顯示以下列:

• 標記:指示當前任務是否已被標記的圖示。 您可以單擊此欄位來標記或取消標記任務。
• ID:任務的ID
• 狀態:任務的Task.Status 屬性
• 開始時間(秒):這表示任務在除錯會話中啟動了多少秒
• 持續時間(秒):這表示任務已經執行了多長時間
• 位置:這顯示執行緒上任務的呼叫堆疊位置
• 任務:任務開始的初始方法。 已傳遞的任何引數也將顯示在此欄位中。

透過在視窗中右鍵單擊並選擇“列”,可以顯示其他幾個隱藏欄位:

圖 10.18 – 在“任務”視窗中新增或刪除列

image

您可以在“任務”視窗中對任務進行排序和分組,類似於“執行緒”視窗的工作方式。不同之處在於“任務”視窗沒有工具欄。 所有操作均透過右鍵單擊上下文選單執行。

除錯並行 .NET 程式碼時可以使用的另一個工具是“除錯位置”工具欄。 如果它尚未顯示在 Visual Studio 中,您可以透過轉到“檢視”|“開啟它”來開啟它。 工具欄| 除錯位置。除錯時,工具欄功能會亮起:

圖 10.19 – 除錯時檢視“除錯位置”工具欄

image

從工具欄中,您可以選擇活動的程序、執行緒和堆疊幀。 切換當前所選執行緒的標記狀態也很容易。

相關文章