2023年9月27日 星期三

在 C# 內,透過 PLinq 的 AsParallel 方法並且使用 WithDegreeOfParallelism 指定平行度做到多執行緒與平行化處理剖析

在 C# 內,透過 PLinq 的 AsParallel 方法並且使用 WithDegreeOfParallelism 指定平行度做到多執行緒與平行化處理剖析

延續上一篇 在 C# 內,透過 PLinq 的 AsParallel 方法做到多執行緒與平行化處理剖析 文章,在這篇文章提到了想要使用 PLinq 的 AsParallel 語法,讓原先的 Linq 查詢由同步查詢,頓時轉化成為平行查詢;可是,往往事與願違,似乎沒有達到如預期的效果,因此,開發人員便化身成為 柯南 ,自己假設、想像各種理由,論述出一個讓自己與他人相信的講法,可是,許多情況卻不是這樣。

若開發者的電腦有八個邏輯處理器,並且使用 PLinq 語法做到平行查詢,此時,理論上這個查詢的速度應該可以達到八倍左右,因為,PLinq 會將原先的列舉所有項目,切割成為八個 Chunk,接著會由八個執行緒來分別做這八個 Chunk 項目的查詢工作。

這裡要注意到的是:

  • 不是所有的 Linq 敘述改成平行查詢,執行速度都會大幅提升到你想像到的
  • Linq 是屬於 CPU Bound 密集的計算作業,因此,若查詢過程用到大量 I/O Bound 的計算作業,建議可以採用其他的 TPL 工作平行程式庫 所提供的作法。
  • PLinq 可以提供的最大平行度,預設將會取決於當時執行程式碼的這台電腦上的硬體架構。
  • 經過平行查詢處理之後,所得到的集合項目順序,可能會有所不同。

在這篇文章中,將會探討 PLinq 平行度的問題,所謂平行計算中的平行度,將指的是可以同時執行的運算數量。平行計算的目標是利用多個處理器或核心來加快計算速度,因此平行度越高,計算速度就越快。

平行度可以分為以下幾個方面:

  • 指令層級平行度:指的是在一個程式運行中,可以同時執行的指令數量。指令層級平行度可以通過流水線、超標量等技術來提高。
  • 資料平行度:指的是可以同時處理的資料數量。資料平行度可以通過將資料分割成小塊,並分配給不同的處理器或核心來提高。
  • 任務平行度:指的是可以同時執行的任務數量。任務平行度可以通過將任務分割成小塊,並分配給不同的處理器或核心來提高。

所以,接下來將會透過 PLinq 所提供的 WithDegreeOfParallelism 方法,觀察與了解在特定硬體架構下,選擇使用不同平行度將會有甚麼問題。在微軟官方文件中,對於 WithDegreeOfParallelism 的解釋為:設定於查詢中使用的平行處理原則程度。 平行處理原則的程度,就是可在處理查詢時同步執行的最大作業數目

建立 PLinq 與使用 WithDegreeOfParallelism 方法 測試專案

為了簡化測試用專案的複雜度,因此,在這裡將會建立一個 Console 主控台應用類型的專案。

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [主控台]
  • 在中間的專案範本清單中,找到並且點選 [主控台應用程式] 專案範本選項

    專案,用於建立可在 Windows、Linux 及 macOS 於 .NET 執行的命令列應用程式

  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 csWithDegreeOfParallelism 作為專案名稱
  • 在剛剛輸入的 [專案名稱] 欄位下方,確認沒有勾選 [將解決方案與專案至於相同目錄中] 這個檢查盒控制項
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 在 [架構] 欄位中,請選擇最新的開發框架,這裡選擇的 [架構] 是 : .NET 7.0 (標準字詞支援)
  • 在這個練習中,需要去勾選 [不要使用最上層陳述式(T)] 這個檢查盒控制項

    這裡的這個操作,可以由讀者自行決定是否要勾選這個檢查盒控制項

  • 請點選右下角的 [建立] 按鈕

稍微等候一下,這個主控台專案將會建立完成

撰寫測試用的程式碼

  • 在此專案節點下,找到並且打開 [Program.cs] 這個檔案
  • 使用底下 C# 程式碼替換掉 [Program.cs] 檔案內所有程式碼內容
namespace csWithDegreeOfParallelism;

internal class Program
{
    static void Main(string[] args)
    {
        //ThreadPool.SetMinThreads(20, 20);
        var source = Enumerable.Range(1, 20);

        var evenNums = source.AsParallel()
            .WithDegreeOfParallelism(4)
            .Select(ShowInfo);

        Console.WriteLine($"Total {evenNums.Count()}");
    }

    static int ShowInfo(int n)
    {
        Console.WriteLine($"N={n:d6} {DateTime.Now:mm:ss} / Thread ID {Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(5000);
        return n;
    }
}

在這裡將會使用 [Enumerable.Range(1, 20)] 方法,產生出 20 個整數數值列舉物件, source ,接著對這個列著列舉物件下達 AsParallel() 方法呼叫,讓這個 Linq 敘述,轉換成為要使用 PLinq 平行處理的計算方式。

緊接著使用了 WithDegreeOfParallelism(4) 方法,宣告這個 PLinq 查詢的平行度為 4

最後,使用了 Select(ShowInfo) 方法,產生出新的列舉值,不過,這裡所生成的列舉值,將會使用平行處理方式來處理。

對於 [ShowInfo] 這個方法,將會使用 Thread.Sleep(5000) 方法,模擬要產生 PDF 或者 HTML 檔案所要花費的時間,而在這個方法之前,將會列出所接收到的列舉項目的數值(整數值),當時的時間,當前所用的執行緒 ID。

現在開始來觀察各種不同平行度下的查詢結果

在 4 個邏輯處理器下 與 WithDegreeOfParallelism(4) 的運行結果

在這裡將會準備了一台具有四個邏輯處理器的電腦,所有的實驗都會在這台電腦上來運行,這台電腦上僅安裝了最基本的 Windows 10 最新更新的作業系統,透過 Visual Studio 所開發出來的測試程式專案,將會透過發佈過程,取得所產生的獨立執行檔案,並且將這個檔案複製到這台四個邏輯處理器的電腦來執行。

將上述的專案採用底下的模式來發布

  • 採用發佈到資料夾模式
  • 部署模式設定為 獨立式
  • 目標執行階段設定為 win-x64
  • 檔案發行選項內要勾選 產生單一檔案 與 修剪未使用的程式碼
  • 將發布後的兩個檔案複製到這台主機上

透過命令提示字元視窗來執行這個 csWithDegreeOfParallelism.exe 程式,將會看到底下輸出結果

C:\Vulcan\win-x64>csWithDegreeOfParallelism.exe
N=000003 52:42 / Thread ID 7
N=000004 52:42 / Thread ID 6
N=000001 52:42 / Thread ID 1
N=000002 52:42 / Thread ID 4
N=000005 52:47 / Thread ID 7
N=000007 52:47 / Thread ID 1
N=000006 52:47 / Thread ID 4
N=000008 52:47 / Thread ID 6
N=000009 52:52 / Thread ID 7
N=000010 52:52 / Thread ID 6
N=000012 52:52 / Thread ID 1
N=000011 52:52 / Thread ID 4
N=000013 52:57 / Thread ID 7
N=000014 52:57 / Thread ID 4
N=000016 52:57 / Thread ID 6
N=000015 52:57 / Thread ID 1
N=000017 53:02 / Thread ID 7
N=000018 53:02 / Thread ID 4
N=000020 53:02 / Thread ID 1
N=000019 53:02 / Thread ID 6
Total 20

從執行結果得到底下結論

  • 因為在 PLinq 語法中,有使用 WithDegreeOfParallelism 方法來指定平行度為 4,因此,PLinq 在執行的時候將會把 20 個項目切割成為 4 個 Chunk
  • 將會有 4 個執行緒分別來處理不同 Chunk 內的項目物件
  • 由於這台主機具備 4 個邏輯處理器,若沒有使用 WithDegreeOfParallelism 方法,所得到的執行結果也是相同的
  • 在這個列舉物件內的 20 個整數項目中,分別由四個執行緒,在 52:42 , 52:47 , 52:52 , 52:57 , 53:02 五個階段,進行查詢作業
  • 由於透過 4 個執行緒平行執行,可以讓原先需要耗費 20 * 5 (秒) = 100 秒的工作,改善成為 5 * 5 (秒) = 25 秒,使得整體執行速度提升了四倍

在 4 個邏輯處理器下 與 WithDegreeOfParallelism(2) 的運行結果

將此專案原始碼的 WithDegreeOfParallelism 方法修改成為 WithDegreeOfParallelism(2)

請重新發佈此專案,並將生成最終執行檔案複製到測試用的主機上

透過命令提示字元視窗來執行這個 csWithDegreeOfParallelism.exe 程式,將會看到底下輸出結果

C:\Vulcan\win-x64>csWithDegreeOfParallelism.exe
N=000002 56:08 / Thread ID 4
N=000001 56:08 / Thread ID 1
N=000003 56:13 / Thread ID 1
N=000004 56:13 / Thread ID 4
N=000005 56:18 / Thread ID 4
N=000006 56:18 / Thread ID 1
N=000007 56:23 / Thread ID 4
N=000008 56:23 / Thread ID 1
N=000009 56:28 / Thread ID 4
N=000010 56:28 / Thread ID 1
N=000011 56:33 / Thread ID 4
N=000012 56:33 / Thread ID 1
N=000013 56:38 / Thread ID 4
N=000014 56:38 / Thread ID 1
N=000015 56:43 / Thread ID 4
N=000016 56:43 / Thread ID 1
N=000017 56:48 / Thread ID 4
N=000019 56:48 / Thread ID 1
N=000018 56:53 / Thread ID 4
N=000020 56:53 / Thread ID 1
Total 20

從執行結果得到底下結論

  • 此時,這個 PLinq 將會採用平行度為 2 的方式來執行,也就是說,將會使用兩個執行緒來執行這次的查詢作業
  • 由於這台執行的主機具備有 4 個邏輯處理器,因此,當平行度低於這台主機的所有邏輯處理器數量的時候,沒有太多問題發生,這是因為每個要進行查詢的後,將會使用所指定平行度數量的執行緒來平行運算。
  • 從底下執行結果可以看出,在這個列舉物件內的 20 個整數項目中,分別由 2 個執行緒,在 56:08 , 56:13 , 56:18 , 56:23 , 56:28 , 56:33 , 56:38 , 56:43 , 56:48 , 56:53 10 個階段,進行查詢作業
  • 由於透過 2 個執行緒平行執行,可以讓原先需要耗費 20 * 5 (秒) = 100 秒的工作,改善成為 10 * 5 (秒) = 50 秒,使得整體執行速度提升了 2 倍

在 4 個邏輯處理器下 與 WithDegreeOfParallelism(8) 的運行結果

將此專案原始碼的 WithDegreeOfParallelism 方法修改成為 WithDegreeOfParallelism(8)

此時,這個 PLinq 將會採用平行度為 8 的方式來執行,也就是說,將會使用 8 個執行緒來執行這次的查詢作業(問題真的是這樣嗎?和你想像的相同的嗎?)

請重新發佈此專案,並將生成最終執行檔案複製到測試用的主機上

透過命令提示字元視窗來執行這個 csWithDegreeOfParallelism.exe 程式,將會看到底下輸出結果

C:\Vulcan\win-x64>csWithDegreeOfParallelism.exe
N=000003 58:20 / Thread ID 8
N=000002 58:20 / Thread ID 4
N=000004 58:20 / Thread ID 7
N=000005 58:20 / Thread ID 6
N=000001 58:20 / Thread ID 1
N=000006 58:20 / Thread ID 9
N=000007 58:21 / Thread ID 10
N=000008 58:22 / Thread ID 11
N=000012 58:25 / Thread ID 8
N=000010 58:25 / Thread ID 7
N=000011 58:25 / Thread ID 6
N=000013 58:25 / Thread ID 4
N=000009 58:25 / Thread ID 1
N=000014 58:25 / Thread ID 9
N=000015 58:26 / Thread ID 10
N=000016 58:27 / Thread ID 11
N=000018 58:30 / Thread ID 6
N=000019 58:30 / Thread ID 4
N=000020 58:30 / Thread ID 7
N=000017 58:30 / Thread ID 1
Total 20

從執行結果得到底下結論

  • 此時,這個 PLinq 將會採用平行度為 8 的方式來執行,也就是說,將會使用 8 個執行緒來執行這次的查詢作業
  • 當在使用 PLinq 進行平行查詢的時候,需要用到的額外執行緒,將會透過 執行緒集區 來取得
  • 由於這台執行的主機具備有 4 個邏輯處理器,因此,當有需要透過執行緒集區取得執行緒的時候,只要在 4 個執行緒內,執行緒集區會立刻提供給應用程式來使用,反之,若需要的執行緒已經超過 4 個以上,此時,執行緒集區將會採用執行緒注入的方式來提供可用的執行緒物件
  • 執行緒集區注入執行緒的時間,在每個 .NET Runtime 皆會有可能有些差異,原則上,將會落在 0.5 秒到 1 秒之間才會注入一個執行緒到執行緒集區內
  • 當平行度低於這台主機的所有邏輯處理器數量的時候,沒有太多問題發生,在這裡的測試情況是,指定的平行度將會大於台主機的所有邏輯處理器數量,從執行結果輸出文字,可以看到執行緒注入的情況發生
  • 從底下執行結果可以看出,在 58:20 這個時間點已經有 6 個執行緒用於平行查詢之用(為什麼一開始不是 4 個執行緒呢?關於這點,留給讀者來自行思考?)
  • 在 58:21 , 58:22 這兩個時間點,又分別注入了兩個執行緒到執行緒集區內
  • 在第一次啟動這個平行查詢之後的五秒後,這些平行查詢已經都完成了,所以,可以看到在 58:25 這個時間點又有六個執行緒(之前完成查詢的執行緒,因為用完了,就會歸還給執行緒集區,而此時又觸發需要執行緒,所以,就又從執行緒集區取得這六個執行緒來作為查詢計算之用)
  • 同理,在 58:26 , 58:27 這兩個時間點,因為之前的執行緒執行完成了(已經過了五秒),這兩個執行緒用完後就會歸還給執行緒集區,因此,當又需要透過執行緒集區取得執行緒的時後,就不再需要透過注入的方式取得,而是直接使用在執行緒集區內快取的執行緒來使用
  • 原則上,執行緒集區內的快取執行緒,大約會保留約 20~30 秒左右
  • 由於透過平行度為 8 來平行執行查詢,原本期望可以在 20 / 8 = 2.5 ,約 5(秒) * 3 = 15 秒內完成所有的查詢工作,可是,事實上好像不是這樣

在 4 個邏輯處理器下 與 WithDegreeOfParallelism(20) 是否有機會在 5 秒內完成查詢計算呢?

將此專案原始碼的 WithDegreeOfParallelism 方法修改成為 WithDegreeOfParallelism(20)

找到 ThreadPool.SetMinThreads(20, 20); 敘述,將其解除註解

請重新發佈此專案,並將生成最終執行檔案複製到測試用的主機上

透過命令提示字元視窗來執行這個 csWithDegreeOfParallelism.exe 程式,將會看到底下輸出結果

C:\Vulcan\win-x64>csWithDegreeOfParallelism.exe
N=000016 01:11 / Thread ID 19
N=000020 01:11 / Thread ID 23
N=000008 01:11 / Thread ID 11
N=000005 01:11 / Thread ID 8
N=000011 01:11 / Thread ID 14
N=000010 01:11 / Thread ID 13
N=000002 01:11 / Thread ID 4
N=000017 01:11 / Thread ID 20
N=000006 01:11 / Thread ID 9
N=000001 01:11 / Thread ID 1
N=000003 01:11 / Thread ID 6
N=000007 01:11 / Thread ID 10
N=000018 01:11 / Thread ID 21
N=000009 01:11 / Thread ID 12
N=000019 01:11 / Thread ID 22
N=000012 01:11 / Thread ID 15
N=000015 01:11 / Thread ID 18
N=000013 01:11 / Thread ID 16
N=000004 01:11 / Thread ID 7
N=000014 01:11 / Thread ID 17
Total 20

從執行結果得到底下結論

  • 首先 ThreadPool.SetMinThreads(20, 20) 將會宣告執行緒集區對於前 20 個執行緒使用請求,可以直接提供,不需要使用爬山理論的方式來注入執行緒。
  • 此時,這個 PLinq 將會採用平行度為 20 的方式來執行,也就是說,將會使用 20 個執行緒來執行這次的查詢作業,不管當時的硬體 CPU 邏輯處理器核心數量有多少
  • 當然,這樣強制修改執行緒集區預設最小提供執行緒數量的作法,有好處也會其他的副作用,這個時候需要由當前開發者自己決定
  • 透過實際執行結果可以看到,在次這 PLinq 查詢,因為平行度為 20,執行緒集區最小可用執行緒數量為 20,並且這個列舉長度也為 20
  • 所以,在執行 Linq 查詢的時候,就會有 20 個執行緒出現,全部執行時間就是 5 秒鐘

 





2023年9月26日 星期二

觀察 .NET CLR 執行緒或執行緒集區與作業系統內執行緒的關係

觀察 .NET CLR 執行緒或執行緒集區與作業系統內執行緒的關係

當使用 C# 開發出一套可以在 .NET CLR 環境下運行的程式,必定會遇到執行緒這樣的議題,然而,對於執行緒這個物件而言,這是由作業系統的核心模式內所提供的一個物件,對於執行緒而言,執行緒代表了程式執行的一個獨立單元。執行緒可以同時執行在同一個處理器上,以提高系統的運行效率。在 Windows OS 中,執行緒是作業系統進行資源分配和調度的一個基本單位。作業系統會根據執行緒的優先級來調度執行緒,以確保系統的穩定運行。

不過,.NET CLR 下的執行緒與 Windows 作業系統內的執行緒都是用來在同一個處理器上同時執行多個任務的工具。但是,兩者在控制權、記憶體管理、以及同步等方面存在一些差異。

關於 .NET CLR 下的執行緒 : .NET CLR 下的執行緒是由 CLR 控制的。CLR 會根據執行緒的優先級來調度執行緒,並且會在執行緒之間進行同步。CLR 會為每個執行緒分配一個獨立的記憶體空間,並且會在執行緒結束時釋放記憶體。CLR 提供了多種同步機制來防止執行緒間的資料競爭。

關於 Windows 作業系統內的執行緒 : Windows 作業系統內的執行緒是由作業系統控制的。作業系統會根據 CPU 的空閒情況來調度執行緒,並且會在執行緒之間進行同步。

.NET CLR 下的執行緒提供了更高的抽象性,可以讓開發人員專注於應用程式的邏輯,而不需要擔心底層的細節。Windows 作業系統內的執行緒提供了更大的控制權,可以讓開發人員根據自己的需求來調整執行緒的行為。

.NET CLR 下的執行緒與 Windows 作業系統內的執行緒存在著對應關係。

在 .NET CLR 下,每個執行緒都會對應到一個 Windows 作業系統內的執行緒。CLR 會將 .NET CLR 下的執行緒映射到 Windows 作業系統內的執行緒,並且會負責在兩者之間進行轉換。CLR 會根據 .NET CLR 下的執行緒的狀態來管理 Windows 作業系統內的執行緒。例如,如果 .NET CLR 下的執行緒被終止,則 CLR 會終止對應的 Windows 作業系統內的執行緒。

因此,可以說 .NET CLR 下的執行緒是 Windows 作業系統內執行緒的抽象。應用程式可以使用 .NET CLR 下的執行緒來編寫多執行緒程式,而不需要直接處理 Windows 作業系統內的執行緒。

不過,在 .NET CLR 下的執行集區與作業系統間的關係又是如何呢?我們知道一旦一個 .NET 應用程式啟動之後,在 .NET 下的執行緒集區理論上應該有相當於該主機邏輯處理器數量的執行緒存在,但是,事實是否是這樣了,這裡也想藉由這篇文來更深的探討 .NET 執行緒集區與作業系統內的執行緒關係。

下載與安裝 SysInternals 的 Process Explorer 工具

這篇文章將會透一個小範例程式碼,搭配 SysInternals 的 Process Explorer 這個工具,來觀察 .NET 執行緒集區與作業系統內的執行緒關係。

請先透過底下的連結,下載 SysInternals 的 Process Explorer這個工具,並且安裝起來。

https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer

其實這個工具是不需要做任何安裝與設定工作的,只要下載下來,然後解壓縮,就可以直接使用了。

這個下載壓縮檔案內將會如下圖所示的內容

在我這台電腦上安裝的是 64 位元的作業系統,所以我會使用 [procexp64.exe] 這個執行檔來執行這個工具。

執行這個 Process Exprorer , procexp64.exe , 檔案,將會看到底下畫面

這是 SysInternals 提供的 Process Exprorer 執行畫面截圖,在這個畫面中,將會列出這台作業系統內所有的處理程序 Process 物件清單,請在這裡隨意點選一個處理程序,在這裡例子,點選是任何一個 chrome.exe 處理程序;當點選了一個處理程序之後,這個處理程序將會顯示違背景色為藍色的模式。

現在,想要知道這個處理程序在作業系統內已經使用到多少的執行緒,接下來僅需要按下 [Ctrl] + [L] 組合按鍵,就會在下方出現一個新的面板,在此面板上有三個分頁頁次,分別是 [Handles] , [Dlls] , [Threads],此時顯示的正是執行緒分頁;這代表了這個處理程序擁有的執行緒。

在 [Threads] 分頁面板內,所以雙擊任何一個執行緒,將可以看到該執行緒的呼叫堆疊資訊,如下圖所示

建立 Console 類型專案

請依照底下的操作,建立起這篇文章需要用到的練習專案

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [主控台]
  • 在中間的專案範本清單中,找到並且點選 [主控台應用程式] 專案範本選項

    專案,用於建立可在 Windows、Linux 及 macOS 於 .NET 執行的命令列應用程式

  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 AWatchThreadOnSystem 作為專案名稱
  • 在剛剛輸入的 [專案名稱] 欄位下方,確認沒有勾選 [將解決方案與專案至於相同目錄中] 這個檢查盒控制項
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 在 [架構] 欄位中,請選擇最新的開發框架,這裡選擇的 [架構] 是 : .NET 7.0 (標準字詞支援)
  • 在這個練習中,需要去勾選 [不要使用最上層陳述式(T)] 這個檢查盒控制項

    這裡的這個操作,可以由讀者自行決定是否要勾選這個檢查盒控制項

  • 請點選右下角的 [建立] 按鈕

稍微等候一下,這個主控台專案將會建立完成

安裝要用到的 NuGet 開發套件

因為開發此專案時會用到這些 NuGet 套件,請依照底下說明,將需要用到的 NuGet 套件安裝起來。

安裝 Microsoft.Diagnostics.Runtime 套件

  • 滑鼠右擊 [方案總管] 視窗內的 [專案節點] 下方的 [相依性] 節點
  • 從彈出功能表清單中,點選 [管理 NuGet 套件] 這個功能選項清單
  • 此時,將會看到 [NuGet: AWatchThreadOnSystem] 視窗
  • 切換此視窗的標籤頁次到名稱為 [瀏覽] 這個標籤頁次
  • 在左上方找到一個搜尋文字輸入盒,在此輸入 Microsoft.Diagnostics.Runtime
  • 稍待一會,將會在下方看到這個套件被搜尋出來
  • 點選 [Microsoft.Diagnostics.Runtime] 套件名稱
  • 在視窗右方,將會看到該套件詳細說明的內容,其中,右上方有的 [安裝] 按鈕
  • 點選這個 [安裝] 按鈕,將這個套件安裝到專案內

建立要 執行緒 關係的程式碼

  • 在 [方案總管] 內找到並且開啟 [Program.cs] 檔案這個節點
  • 使用底下 C# 程式碼,將原本的程式碼取代掉
using Microsoft.Diagnostics.Runtime;
using System.Diagnostics;

namespace AWatchThreadOnSystem
{
    internal class Program
    {
        private const int MaxSleepTime = 5000;
        private const int MaxThreads = 8;

        static void Main(string[] args)
        {
            #region 等候使用者指令,決定要執行哪個工作
            while (true)
            {
                var key = Console.ReadKey();
                if (key.Key == ConsoleKey.S)
                {
                    ShowThreadInformation();
                }
                else if (key.Key == ConsoleKey.P)
                {
                    UsingThreadPool();
                }
                else if (key.Key == ConsoleKey.E)
                {
                    // 結束程式
                    Environment.Exit(0);
                }
            }
            #endregion
        }

        /// <summary>
        /// 使用執行緒集區來執行工作
        /// </summary>
        private static void UsingThreadPool()
        {
            for (int i = 0; i < MaxThreads; i++)
            {
                // 透過執行緒集區來執行工作,並且模擬會休息一段時間,讓我們可以觀察到執行緒的變化
                ThreadPool.QueueUserWorkItem((state) =>
                {
                    Console.WriteLine($"  Pool Managed Id : {Thread.CurrentThread.ManagedThreadId}");
                    Thread.Sleep(MaxSleepTime);
                    Console.WriteLine($"  Pool Managed Id : {Thread.CurrentThread.ManagedThreadId} Exit");
                });
            }
        }

        /// <summary>
        /// 顯示處理程序執行緒與 .NET Managed 執行緒資訊到螢幕上
        /// </summary>
        private static void ShowThreadInformation()
        {
            int threadCount = 1;

            #region Get Process's Threads - 使用 .NET 提供的 API 來抓取相關資訊
            Console.WriteLine($"顯示該處理程序內在 作業系統 上的所有執行緒資訊");
            var osThreads = System.Diagnostics.Process.GetCurrentProcess().Threads;
            threadCount = 1;
            foreach (ProcessThread itemProcessThread in osThreads)
            {
                Console.WriteLine($"OS Thread {threadCount++} : Id {itemProcessThread.Id} " +
                    $"{itemProcessThread.PriorityLevel.ToString()}");
            }
            #endregion

            #region Get CLR Managed Threads - 這裡是透過第三方套件來讀取到這些資訊
            Console.WriteLine($"顯示該處理程序內在 CLR Managed 上的所有執行緒資訊");
            threadCount = 1;
            using (DataTarget target = DataTarget.AttachToProcess(
                Process.GetCurrentProcess().Id, false))
            {
                ClrRuntime runtime = target.ClrVersions.First().CreateRuntime();
                foreach (ClrThread itemClrManagedThread in runtime.Threads)
                {
                    Console.WriteLine($"Managed Thread {threadCount++} : Id" +
                        $"{itemClrManagedThread.ManagedThreadId} (OS Id {itemClrManagedThread.OSThreadId})");
                }
            }
            #endregion
        }
    }
}

在 Main 這個方法內,我們會透過一個無窮迴圈,來等候使用者的指令,決定要執行哪個工作,這裡有三個選項,分別是輸入 S 按鍵,將會顯示處理程序執行緒與 .NET Managed 執行緒資訊到螢幕上;輸入 P 按鍵,將會使用執行緒集區來執行工作;輸入 E 按鍵,將會結束程式。

若輸入 S 按鍵,將會執行 ShowThreadInformation() 方法,這個方法將會顯示處理程序執行緒與 .NET Managed 執行緒資訊到螢幕上,這裡會使用到兩個 API 來取得這些資訊,分別是 System.Diagnostics.Process.GetCurrentProcess().Threads 這個 API 來取得處理程序執行緒資訊,以及使用第三方套件 Microsoft.Diagnostics.Runtime 來取得 .NET Managed 執行緒資訊。

若輸入 P 按鍵,將會執行 UsingThreadPool() 方法,這個方法將會使用執行緒集區來執行工作,這裡會使用到 ThreadPool.QueueUserWorkItem() 這個 API 來執行工作。在此將會依據 MaxThreads 這個物件整數值,啟用多少個執行緒來執行工作,這裡的 MaxThreads 整數值是 8。每個執行緒將會固定休息 MaxSleepTime 時間,模擬該執行緒需要花費這麼多的時間來執行工作。

建立發佈檔案

將上述的專案採用底下的模式來發布

  • 採用發佈到資料夾模式
  • 部署模式設定為 獨立式
  • 目標執行階段設定為 win-x64
  • 檔案發行選項內要勾選 產生單一檔案 與 修剪未使用的程式碼
  • 將發布後所產生的檔案複製到任何一個目錄下

執行程式,觀察結果

開啟命令提示字元視窗,切換到剛剛複製發佈檔案的目錄下

輸入 AWatchThreadOnSystem.exe 執行檔案,將會看到底下畫面

從 Process Explorer 視窗的上方,找到並且點選 AWatchThreadOnSystem.exe 這個處理程序,將會在下方看到有三個標籤頁次,點選到 [Threads] 標籤頁次,將會看到共有 8 個執行緒項目,其 ID 分別為 13468, 9080, 10272, 24020, 17420, 7036, 22056, 21608

不過,稍微等候一段時間,將會看到 [Threads] 標籤頁次內,僅剩下四個執行緒,ID 分別為 13468, 9080, 7036, 22056

現在,在命令提示字元視窗內,輸入 S 按鍵,將會看到底下輸出內容

顯示該處理程序內在 作業系統 上的所有執行緒資訊
OS Thread 1 : Id 9080 Normal
OS Thread 2 : Id 7036 Normal
OS Thread 3 : Id 22056 Normal
OS Thread 4 : Id 13468 Highest
OS Thread 5 : Id 23180 Normal
顯示該處理程序內在 CLR Managed 上的所有執行緒資訊
Managed Thread 1 : Id1 (OS Id 9080)
Managed Thread 2 : Id2 (OS Id 13468)
Managed Thread 3 : Id3 (OS Id 23180)

透過 Process Explorer 將會看到底下內容

從下圖中的工作管理員視窗中,看到這台測試用的主機,共有 8 個邏輯處理器

現在要來透過執行緒集區取得 8 個執行緒,看看在 Process Explorer 上的執行緒標籤內,會顯示甚麼內容

在命令提示字元視窗內,按下按鍵 P ,在 Process Explorer 上的執行緒標籤內將會看到底下內容

而在命令提示字元視窗內,將會顯示

  Pool Managed Id : 6
  Pool Managed Id : 4
  Pool Managed Id : 11
  Pool Managed Id : 8
  Pool Managed Id : 12
  Pool Managed Id : 9
  Pool Managed Id : 7
  Pool Managed Id : 10

5 秒鐘之後,將會看到底下內容顯示在命令提示字元中

  Pool Managed Id : 8 Exit
  Pool Managed Id : 12 Exit
  Pool Managed Id : 9 Exit
  Pool Managed Id : 10 Exit
  Pool Managed Id : 7 Exit
  Pool Managed Id : 4 Exit
  Pool Managed Id : 11 Exit
  Pool Managed Id : 6 Exit

現在按下 P 按鍵後,緊接著按下 S 按鍵,在命令提示字元內,將會看到底下內容

  Pool Managed Id : 4
  Pool Managed Id : 7
  Pool Managed Id : 6
  Pool Managed Id : 8
  Pool Managed Id : 9
  Pool Managed Id : 10
  Pool Managed Id : 11
  Pool Managed Id : 12
顯示該處理程序內在 作業系統 上的所有執行緒資訊
OS Thread 1 : Id 9232 Normal
OS Thread 2 : Id 24452 Normal
OS Thread 3 : Id 6964 Normal
OS Thread 4 : Id 18780 Highest
OS Thread 5 : Id 16464 Normal
OS Thread 6 : Id 292 Normal
OS Thread 7 : Id 24012 Normal
OS Thread 8 : Id 9892 Normal
OS Thread 9 : Id 18784 Normal
OS Thread 10 : Id 19436 Normal
OS Thread 11 : Id 14112 Normal
OS Thread 12 : Id 8908 Normal
OS Thread 13 : Id 6696 Normal
OS Thread 14 : Id 18028 Normal
OS Thread 15 : Id 24976 Normal
OS Thread 16 : Id 17184 Normal
OS Thread 17 : Id 22872 Normal
顯示該處理程序內在 CLR Managed 上的所有執行緒資訊
Managed Thread 1 : Id1 (OS Id 9232)
Managed Thread 2 : Id2 (OS Id 18780)
Managed Thread 3 : Id3 (OS Id 16464)
Managed Thread 4 : Id4 (OS Id 292)
Managed Thread 5 : Id5 (OS Id 24012)
Managed Thread 6 : Id6 (OS Id 9892)
Managed Thread 7 : Id7 (OS Id 18784)
Managed Thread 8 : Id8 (OS Id 19436)
Managed Thread 9 : Id9 (OS Id 14112)
Managed Thread 10 : Id10 (OS Id 8908)
Managed Thread 11 : Id11 (OS Id 6696)
Managed Thread 12 : Id12 (OS Id 18028)
Managed Thread 13 : Id13 (OS Id 22872)
  Pool Managed Id : 8 Exit
  Pool Managed Id : 6 Exit
  Pool Managed Id : 9 Exit
  Pool Managed Id : 10 Exit
  Pool Managed Id : 11 Exit
  Pool Managed Id : 12 Exit
  Pool Managed Id : 7 Exit
  Pool Managed Id : 4 Exit

在經過約 30 秒左右,再度按下 S 按鍵,看到底下內容

顯示該處理程序內在 作業系統 上的所有執行緒資訊
OS Thread 1 : Id 9232 Normal
OS Thread 2 : Id 24452 Normal
OS Thread 3 : Id 6964 Normal
OS Thread 4 : Id 18780 Highest
OS Thread 5 : Id 24012 Normal
OS Thread 6 : Id 24976 Normal
OS Thread 7 : Id 17184 Normal
顯示該處理程序內在 CLR Managed 上的所有執行緒資訊
Managed Thread 1 : Id1 (OS Id 9232)
Managed Thread 2 : Id2 (OS Id 18780)
Managed Thread 3 : Id4 (OS Id 0)
Managed Thread 4 : Id5 (OS Id 24012)
Managed Thread 5 : Id6 (OS Id 0)
Managed Thread 6 : Id7 (OS Id 0)
Managed Thread 7 : Id8 (OS Id 0)
Managed Thread 8 : Id9 (OS Id 0)
Managed Thread 9 : Id10 (OS Id 0)
Managed Thread 10 : Id11 (OS Id 0)
Managed Thread 11 : Id12 (OS Id 0)
Managed Thread 12 : Id13 (OS Id 0)
Managed Thread 13 : Id3 (OS Id 23440) 

將會看到有許多 Managed Thread 存在,但並沒有對應到實際作業系統上的執行緒,因為 OS Id 都為0,這是因為剛剛要到的8個執行緒,是透過執行緒集區取得的,一旦這些執行緒執行完畢之後,約 30 秒左右的時間,將會歸還給作業系統。