2019年8月29日 星期四

在 .NET Core 下, 執行緒集區 Thread Pool的執行緒生成與管理測試

在 .NET Core 下, 執行緒集區 Thread Pool的執行緒生成與管理測試

這篇文章將會透過一個範例程式碼,了解到執行緒集區如何智慧型的管理其持有的執行緒;在所測試的電腦,是個 4 核心且具有超執行緒 Hyper-Threading 的 CPU,因此,對於作業系統而言,將會擁有 8 顆邏輯處理器的狀態,因此,對於一個 .NET 應用程式而言,當這個 .NET 應用程式啟動之後,對於執行緒集區內,將會自動建立起八個 CPU Bound 使用的執行緒與八個 I/O Port 使用的執行緒。

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [ThreadPool Class] 的使用方式



因為,當應用程式需要透過執行緒集區取得多於預設持有持行續的時候,會有什麼狀況發生,進行測試,在這篇文章所使用的開發框架是 .NET Core Console專案。
首先將會使用 FindDefaultThreadOnThreadPool() 方法,取得執行緒集區內所有受管理執行續 ID,並且記錄下來,這樣,等下的測試程式碼,才能夠得知透過執行緒集區取得的執行緒,究竟是執行緒集區原本就預先持有的,還是,執行緒集區另外心產生出來的呢?
接著,這個測試程式將會於不同時間點內,呼叫這個 EmulateMoreThreads 方法,該方法將會透過執行緒集區取得 11 個執行緒,也就是說遠遠超過執行緒集區所預先持有的執行緒,不過,對於執行緒集區並不會受到任何影響,因為超過的 3 個執行緒,執行緒集區將會以每 0.5~1秒鐘的時間,逐漸地建立起這些額外需要的執行緒。另外,對於這這個 EmulateMoreThreads 方法內所取得的執行緒,將會使用 Thread.Sleep 來休息一段時間,才會準備結束執行。
對於 EmulateMoreThreads 方法所取得的執行緒,若該執行緒是該 .NET 應用程式一啟動就在執行緒集區內所配置好的執行緒,將會顯示類似底下的文字,也就是會看到有 ** 兩個星號的符號
要求執行緒作業(5) ** Thread9 從執行緒集區取得該執行緒 10:31:38.9432525
若當前執行的執行緒是執行緒集區新產生出來的,將不會看到有 ** 兩個星號的符號
要求執行緒作業(9) Thread12 是執行緒集區額外新建立的執行緒 10:31:39.9499312
將者將會進行四次的 EmulateMoreThreads 方法呼叫,每次都會使用不同的間隔時間。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

FindDefaultThreadOnThreadPool 蒐集執行緒集區內的所有受管理執行緒 ID 清單

一開始將會執行 FindDefaultThreadOnThreadPool() 方法,在這個方法內,會建立一個迴圈,依據現在執行緒集區內持有的執行緒數量大小,執行一個迴圈,在該迴圈內會從執行緒集區內取得一個執行緒,直到所有執行緒集區內的執行緒都被取出來。
可是,這裡有個需求,那就是當該迴圈內的執行緒之委派方法都執行之後,需要等候訊號通知,以便結束這些委派方法,也就是接著會將這些執行緒歸還到執行緒集區內,還有一個就是,要怎麼知道這些執行緒的委派方法都已經啟動與執行了,並且準備要讀取結束執行的訊號呢?
在執行緒所指定的委派方法內,將會使用 CountdownEvent 、 Semaphore 這個執行緒同步物件,在執行緒指定的委派方法內,將會先顯示一段字串,使用敘述 Console.WriteLine($"Thread{threadId} 已經從執行緒集區取得該執行緒"); ,接著使用 countdown.Signal() 這個方法,告知某個執行緒的委派方法已經成功執行了,接著將會執行 semaphore.WaitOne(); ,等候訊號通知;一旦接收到來自於 Semophore 的訊號,將會使用敘述 Console.WriteLine($"Thread{threadId} 已經歸還給執行緒集區"); 顯示字串,並且結束該委派方法的執行,也就是結束該執行緒的執行。
所以,在迴圈之外,將會使用 countdown.Wait() 來等候所有執行緒集區內的執行緒,都已經正常在執行過程中,只要每個執行緒都有執行到 countdown.Signal() 這個方法,則 countdown.Wait() 這個方法將會繼續往下執行,否則,將會持續使用封鎖式的等候。
緊接著將會使用 semaphore.Release(workerThreads) 方法呼叫,通知所有在等候 semaphore.WaitOne() 訊號的敘述,可以繼續往下執行了,只要 Release 方法一執行,所有的執行緒都會同時收到訊號通知,並且同時繼續往下執行。
在每個執行緒的委派方法內,最重要的一個工作就是要取得當前執行緒所使用的受管理執行緒 ID,這裡將會使用 ThreadsOnThreadPool.TryAdd(threadId, threadId) 敘述來執行,而 ThreadsOnThreadPool 這個物件的宣告型別為 ConcurrentDictionary<int, int>,是個可以用於多執行緒環境下的集合資料字典型別。

EmulateMoreThreads 顯示該執行緒是否為預設執行緒集區持有的執行緒

在這個方法內,將會透過 ThreadsOnThreadPool 這個物件,判斷當前從執行緒集區取得的執行緒,是否為應用程式一起動的時候,所擁有的執行緒,還是執行緒集區另外新產生出來的執行緒;若為原有的執行緒,將會顯示 ** 兩個星號文字,並且休息 defalutSleep ms 的時間,若為執行緒集區新建立的執行緒,則不會看到有任何星號文字出現,並且會休息 NewSleep ms 的時間

執行結果分析

這個測試程式一啟動之後,將會先執行 FindDefaultThreadOnThreadPool() 方法,該方法所執行的工作,前面已久描述了,接著,將會
第一次呼叫 EmulateMoreThreads 方法,從執行輸出結果可以看出,總共會透過執行緒集區取得 11 個執行緒,並且,其中執行緒 9,10,11 這三個執行緒分別間隔一秒鐘的時間,才從執行緒集區取得執行緒,並開始進行執行,其他八個執行緒幾乎是同一個時間就從執行緒集區內取得該執行緒,立即執行。會有這樣的差異那是因為對於這台測試電腦,任何 .NET 應用程式啟動的時候,預設執行緒集區將會擁有八個執行緒物件,因此,前八個要求取得執行緒的需求,可以很快地取得執行緒,後面三個則幾乎是每一秒鐘才會取得所需的執行緒。
接著休息 2 秒鐘,進行第二次呼叫 EmulateMoreThreads 方法,同樣的還是會透過執行緒集區取得 11 個執行緒,從執行結果可以看到出來,這 11 個執行緒的取得與執行委派方法的時間相當的接近,這是因為在剛剛第一次呼叫的時候,執行緒集區已經產生出額外三個執行緒,因此,現在執行緒集區內將會擁有 11 個執行緒,所以,現在當要向執行緒集區要求取得 11 個執行緒,執行緒集區將會立即配置這些執行緒給大家使用,沒有任何延遲問題。
現在將會有個疑問,這三個新建立、產生出來的執行緒,是否會一直存在於執行緒集區內呢?試想一個請況,若有個極端應用,有應用程式在同一個時間內向執行緒集區要求 1000 個執行緒,當然,執行緒集區會依序建立起這些執行緒物件,然而,是否在該應用程式結束之前,這 1000-8=992個執行緒,都會一直存在於系統中呢?
現在,休息 2 秒鐘,進行第三次呼叫 EmulateMoreThreads 方法,在呼叫該方法之前,將會先休息 30 秒鐘之後,才會開始執行這個方法。從執行輸出結果中可以看的出來,這裡所取得的執行緒 ID,與前面的步驟都不盡相同,而且,最後三個執行緒也是間隔一秒鐘之後才會取得;從這裡可以看的出來,之前要求執行緒額外產生的三個執行緒,似乎已經被執行緒集區回收掉了,也就是說,在休息 30 秒之後,執行緒集區內,僅剩下 8 個可用的執行緒可以立即配置與使用。
最後,再度休息 30 秒鐘,進行第四次的 呼叫 EmulateMoreThreads 方法,在呼叫該方法前,會先清空原先找出的執行緒集區內的預設執行ID清單,使用敘述 ThreadsOnThreadPool.Clear() 來完成,然後呼叫 FindDefaultThreadOnThreadPool() 方法重新建立此清單。從這次的執行輸出結果可以看出與第一次的執行結果相同,只不過所顯示的執行緒ID數值是不相同的。
底下是執行結果的所有輸出內容。
等候取得所有執行緒都從執行緒集區取得...
Thread6 已經從執行緒集區取得該執行緒
Thread10 已經從執行緒集區取得該執行緒
Thread4 已經從執行緒集區取得該執行緒
Thread7 已經從執行緒集區取得該執行緒
Thread11 已經從執行緒集區取得該執行緒
Thread8 已經從執行緒集區取得該執行緒
Thread9 已經從執行緒集區取得該執行緒
Thread5 已經從執行緒集區取得該執行緒
準備把取得的執行緒歸還給執行緒集區...
Thread5 已經歸還給執行緒集區
Thread9 已經歸還給執行緒集區
Thread7 已經歸還給執行緒集區
Thread11 已經歸還給執行緒集區
Thread4 已經歸還給執行緒集區
Thread6 已經歸還給執行緒集區
Thread8 已經歸還給執行緒集區
Thread10 已經歸還給執行緒集區


第 1 次,產生 11 執行緒請求


對於預先配置執行緒將休息 4000 ms, 對於新產生的執行緒將休息 4000 ms

要求執行緒作業(1) ** Thread10 從執行緒集區取得該執行緒 11:40:11.8510015
要求執行緒作業(8) ** Thread5 從執行緒集區取得該執行緒 11:40:11.8510139
要求執行緒作業(2) ** Thread8 從執行緒集區取得該執行緒 11:40:11.8510584
要求執行緒作業(4) ** Thread6 從執行緒集區取得該執行緒 11:40:11.8510709
要求執行緒作業(7) ** Thread9 從執行緒集區取得該執行緒 11:40:11.8510375
要求執行緒作業(5) ** Thread11 從執行緒集區取得該執行緒 11:40:11.8510057
要求執行緒作業(3) ** Thread4 從執行緒集區取得該執行緒 11:40:11.8510802
要求執行緒作業(6) ** Thread7 從執行緒集區取得該執行緒 11:40:11.8510473
要求執行緒作業(9) Thread12 是執行緒集區額外新建立的執行緒 11:40:12.8498194
要求執行緒作業(10) Thread13 是執行緒集區額外新建立的執行緒 11:40:13.8498459
要求執行緒作業(11) Thread14 是執行緒集區額外新建立的執行緒 11:40:14.8508899
要求執行緒作業(4) ** Thread6 準備結束執行 11:40:15.8544696
要求執行緒作業(1) ** Thread10 準備結束執行 11:40:15.8544696
要求執行緒作業(2) ** Thread8 準備結束執行 11:40:15.8544722
要求執行緒作業(8) ** Thread5 準備結束執行 11:40:15.8544744
要求執行緒作業(7) ** Thread9 準備結束執行 11:40:15.8555398
要求執行緒作業(5) ** Thread11 準備結束執行 11:40:15.8555436
要求執行緒作業(6) ** Thread7 準備結束執行 11:40:15.8564553
要求執行緒作業(3) ** Thread4 準備結束執行 11:40:15.8564554
要求執行緒作業(9)  Thread12 準備結束執行 11:40:16.8504807
要求執行緒作業(10)  Thread13 準備結束執行 11:40:17.8505184
要求執行緒作業(11)  Thread14 準備結束執行 11:40:18.8515355


第 2次,產生 11 執行緒請求


對於預先配置執行緒將休息 5000 ms, 對於新產生的執行緒將休息 5000 ms

要求執行緒作業(1) Thread14 是執行緒集區額外新建立的執行緒 11:40:20.8562061
要求執行緒作業(4) ** Thread7 從執行緒集區取得該執行緒 11:40:20.8562618
要求執行緒作業(5) ** Thread4 從執行緒集區取得該執行緒 11:40:20.8562618
要求執行緒作業(3) Thread12 是執行緒集區額外新建立的執行緒 11:40:20.8562659
要求執行緒作業(6) ** Thread11 從執行緒集區取得該執行緒 11:40:20.8562662
要求執行緒作業(2) Thread13 是執行緒集區額外新建立的執行緒 11:40:20.8562476
要求執行緒作業(7) ** Thread9 從執行緒集區取得該執行緒 11:40:20.8562734
要求執行緒作業(8) ** Thread5 從執行緒集區取得該執行緒 11:40:20.8562763
要求執行緒作業(9) ** Thread8 從執行緒集區取得該執行緒 11:40:20.8562824
要求執行緒作業(11) ** Thread6 從執行緒集區取得該執行緒 11:40:20.8562873
要求執行緒作業(10) Thread15 是執行緒集區額外新建立的執行緒 11:40:20.8562993
要求執行緒作業(4) ** Thread7 準備結束執行 11:40:25.8673359
要求執行緒作業(3)  Thread12 準備結束執行 11:40:25.8673823
要求執行緒作業(7) ** Thread9 準備結束執行 11:40:25.8673878
要求執行緒作業(8) ** Thread5 準備結束執行 11:40:25.8674149
要求執行緒作業(9) ** Thread8 準備結束執行 11:40:25.8674217
要求執行緒作業(10)  Thread15 準備結束執行 11:40:25.8674227
要求執行緒作業(5) ** Thread4 準備結束執行 11:40:25.8674644
要求執行緒作業(1)  Thread14 準備結束執行 11:40:25.8674045
要求執行緒作業(2)  Thread13 準備結束執行 11:40:25.8674931
要求執行緒作業(11) ** Thread6 準備結束執行 11:40:25.8675148
要求執行緒作業(6) ** Thread11 準備結束執行 11:40:25.8675064


第 3次,產生 11 執行緒請求,休息 30 秒


對於預先配置執行緒將休息 4000 ms, 對於新產生的執行緒將休息 4000 ms

要求執行緒作業(1) ** Thread11 從執行緒集區取得該執行緒 11:40:57.8726102
要求執行緒作業(2) Thread17 是執行緒集區額外新建立的執行緒 11:40:57.8737710
要求執行緒作業(3) Thread16 是執行緒集區額外新建立的執行緒 11:40:57.8738063
要求執行緒作業(4) Thread18 是執行緒集區額外新建立的執行緒 11:40:57.8739219
要求執行緒作業(5) Thread19 是執行緒集區額外新建立的執行緒 11:40:57.8745854
要求執行緒作業(6) Thread20 是執行緒集區額外新建立的執行緒 11:40:57.8750828
要求執行緒作業(7) Thread21 是執行緒集區額外新建立的執行緒 11:40:57.8765357
要求執行緒作業(8) Thread22 是執行緒集區額外新建立的執行緒 11:40:57.8769389
要求執行緒作業(9) Thread23 是執行緒集區額外新建立的執行緒 11:40:58.8734008
要求執行緒作業(10) Thread24 是執行緒集區額外新建立的執行緒 11:40:59.8744712
要求執行緒作業(11) Thread25 是執行緒集區額外新建立的執行緒 11:41:00.8755105
要求執行緒作業(1) ** Thread11 準備結束執行 11:41:01.8741022
要求執行緒作業(2)  Thread17 準備結束執行 11:41:01.8741131
要求執行緒作業(4)  Thread18 準備結束執行 11:41:01.8750861
要求執行緒作業(3)  Thread16 準備結束執行 11:41:01.8750870
要求執行緒作業(5)  Thread19 準備結束執行 11:41:01.8760643
要求執行緒作業(6)  Thread20 準備結束執行 11:41:01.8770956
要求執行緒作業(7)  Thread21 準備結束執行 11:41:01.8771002
要求執行緒作業(8)  Thread22 準備結束執行 11:41:01.8780994
要求執行緒作業(9)  Thread23 準備結束執行 11:41:02.8740839
要求執行緒作業(10)  Thread24 準備結束執行 11:41:03.8750737
要求執行緒作業(11)  Thread25 準備結束執行 11:41:04.8760519


休息 30 秒,等候執行緒集區清空新建立的執行緒


第 4次,產生 11 執行緒請求
Thread25 已經從執行緒集區取得該執行緒
Thread26 已經從執行緒集區取得該執行緒


等候取得所有執行緒都從執行緒集區取得...
Thread27 已經從執行緒集區取得該執行緒
Thread29 已經從執行緒集區取得該執行緒
Thread28 已經從執行緒集區取得該執行緒
Thread30 已經從執行緒集區取得該執行緒
Thread31 已經從執行緒集區取得該執行緒
Thread32 已經從執行緒集區取得該執行緒
準備把取得的執行緒歸還給執行緒集區...
Thread30 已經歸還給執行緒集區
Thread26 已經歸還給執行緒集區
Thread27 已經歸還給執行緒集區
Thread29 已經歸還給執行緒集區
Thread31 已經歸還給執行緒集區
Thread28 已經歸還給執行緒集區
Thread32 已經歸還給執行緒集區
Thread25 已經歸還給執行緒集區


對於預先配置執行緒將休息 4000 ms, 對於新產生的執行緒將休息 4000 ms

要求執行緒作業(1) ** Thread25 從執行緒集區取得該執行緒 11:41:36.8911002
要求執行緒作業(2) ** Thread32 從執行緒集區取得該執行緒 11:41:36.8911329
要求執行緒作業(3) ** Thread28 從執行緒集區取得該執行緒 11:41:36.8911674
要求執行緒作業(4) ** Thread31 從執行緒集區取得該執行緒 11:41:36.8911972
要求執行緒作業(5) ** Thread27 從執行緒集區取得該執行緒 11:41:36.8912271
要求執行緒作業(6) ** Thread26 從執行緒集區取得該執行緒 11:41:36.8912570
要求執行緒作業(8) ** Thread30 從執行緒集區取得該執行緒 11:41:36.8914003
要求執行緒作業(7) ** Thread29 從執行緒集區取得該執行緒 11:41:36.8914003
要求執行緒作業(9) Thread33 是執行緒集區額外新建立的執行緒 11:41:36.8928622
要求執行緒作業(10) Thread34 是執行緒集區額外新建立的執行緒 11:41:37.8943194
要求執行緒作業(11) Thread35 是執行緒集區額外新建立的執行緒 11:41:38.8943503
要求執行緒作業(1) ** Thread25 準備結束執行 11:41:40.8939245
要求執行緒作業(2) ** Thread32 準備結束執行 11:41:40.8939246
要求執行緒作業(3) ** Thread28 準備結束執行 11:41:40.8949295
要求執行緒作業(4) ** Thread31 準備結束執行 11:41:40.8959405
要求執行緒作業(5) ** Thread27 準備結束執行 11:41:40.8959396
要求執行緒作業(6) ** Thread26 準備結束執行 11:41:40.8969167
要求執行緒作業(8) ** Thread30 準備結束執行 11:41:40.8969237
要求執行緒作業(7) ** Thread29 準備結束執行 11:41:40.8979483
要求執行緒作業(9)  Thread33 準備結束執行 11:41:40.8989145
要求執行緒作業(10)  Thread34 準備結束執行 11:41:41.8949817
要求執行緒作業(11)  Thread35 準備結束執行 11:41:42.8949989


Press any key for continuing...

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [ThreadPool Class] 的使用方式





沒有留言:

張貼留言