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] 的使用方式





2019年8月27日 星期二

使用 Task.Run 中使用 CancellationTokenSource 與 CancellationToken 的注意事項

使用 Task.Run 中使用 CancellationTokenSource 與 CancellationToken 的注意事項

CancellationTokenSource Class 所建立的物件,可以取得一個 CancellationTokenSource.Token ,用於 向 CancellationToken 發出訊號,表示應該將它取消,也就是說,當要建立一個非同步應用工作的時候,將會在非同步工作內使用輪詢的方式來檢查,是否有發送出一個 取消執行 的訊號出來,一旦發現到這個訊號,對於非同步工作的程式碼可以選擇:(1) 正常結束與離開這個非同步處理作業 或者是 (2) 使用 CancellationToken.ThrowIfCancellationRequested 拋出一個 OperationCanceledException 例外異常出來,異常終止這個非同步處理作業;當然,不論使用哪種方式,當要終止非同步作業的時候,記得要將已經取得的資源歸還給系統與維持整個處理程序的資料一致性,避免整個處理程序處於不穩定的狀態。

了解更多關於 [Task Class] 的使用方式


了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式



在這篇文章中將會使用 Task.Run 來建立一個非同步工作 (如下面程式碼所列出),不過, Task.Run 方法,有這個多載,需要傳入一個委派方法與一個 CancellationToken 物件;如同前面的說明,在這個委派方法內,需要隨時檢查是否已經發出 取消訊號,這樣可以正常結束委派方法的執行,此時,在委派方法內需要能夠得到 CancellationToken 物件,對於這個 public static System.Threading.Tasks.Task Run (Action action, System.Threading.CancellationToken cancellationToken); 多載方法所傳入的 CancellationToken 物件,在非同步工作的委派方法內,是無法讀取到的,那麼,為什麼已經有在委派方法內檢查了取消權杖的訊號發送狀態,還需要有這樣的多載方法,再次傳入一個取消權杖物件到 Task.Run 方法內呢?
在這篇文章所提到的專案原始碼,可以從 GitHub 下載
C Sharp / C#
CancellationTokenSource cancellationToken = new CancellationTokenSource();
CancellationToken token = cancellationToken.Token;

// 狀況1 : 在非同步工作啟動後一秒種,發出取消執行訊號
cancellationToken.Cancel();

var MyTask = Task.Run(() =>
{
    Console.WriteLine("正在啟動非同步工作");
    Thread.Sleep(5000);
    if (token.IsCancellationRequested)
    {
        Console.WriteLine("非同步工作已經取消了");
    }
    Console.WriteLine("非同步工作結束了");
}, token);

// 狀況2 : 在非同步工作啟動後一秒種,發出取消執行訊號
//Thread.Sleep(1000);
// 狀況3 : 在非同步工作啟動後一秒種,發出取消執行訊號
//cancellationToken.Cancel();

Console.WriteLine("按下任一按鍵,檢查工作狀態");
Console.ReadKey();

Console.WriteLine($"工作狀態 {MyTask.Status}");

Console.WriteLine("Press any key for continuing...");
Console.ReadKey();

情境一:在啟動非同步工作前,就已經發出取消訊號

  • 請將狀況1 底下的 cancellationToken.Cancel(); 解除註解
  • 請將狀況2, 狀況3 後的程式碼註解起來,這包括了 Thread.Sleep(1000); 與 cancellationToken.Cancel();
  • 現在,請執行這個專案,底下是執行結果輸出內容
按下任一按鍵,檢查工作狀態
 工作狀態 Canceled
Press any key for continuing...
從執行結果可以看出,因為在非同步工作建立之前,就已經執行 cancellationToken.Cancel() 方法,發出了取消訊號,而在 Task.Run 的方法引數也有傳遞 CancellationToken 進去,因此,這個非同步工作所指定的委派方法尚未執行前,就已經取消了此非同步工作。
那麼,為什麼要有這樣的設計呢?這是因為要啟動與執行一個非同步工作是相當耗費計算機執行成本的,所以,若在執行非同步工作前,就已經發出取消訊號,最好的做法就是不要去執行非同步工作指定的委派方法,就直接取消非同步工作的執行,要不然,就需要開始執行非同步委派方法,等到該委派方法內才能夠檢查是否已經發出取消訊號了。

情境二:Task.Run 不要傳送 CancellationToken

  • 維持上述的程式碼,請將 Task.Run 方法內的第二個引數 , token,暫時移除
  • 請再度執行該專案
按下任一按鍵,檢查工作狀態
正在啟動非同步工作
 工作狀態 Running
Press any key for continuing...
非同步工作已經取消了
非同步工作結束了
從上述執行結果內容可以看出,雖然一開就已經送出取消訊號,還是會進入到非同步委派方法內來執行,所以會看到 正在啟動非同步工作 字串顯示在螢幕上,緊接著休息五秒鐘,才會檢查是否發送出取消訊號。這與狀況一的執行結果絕然不同,因此,若取消權杖在非同步工作啟動與執行前,就已經發送出訊號之後,最好是在 Task.Run 方法內,將取消權杖傳送進去。

情境三:執行 Task.Run 之後,立即送出權杖取消訊號

  • 請將狀態2 底下的 Thread.Sleep(1000); 程式碼註解
  • 請再度將 Task.Run 方法內,加入第二個引數 , token
  • 請將狀態3 後的程式碼解除註解,也就是 cancellationToken.Cancel();
  • 請再度執行該專案
按下任一按鍵,檢查工作狀態
 工作狀態 Canceled
Press any key for continuing...
在這裡,發送取消訊號,也就是 cancellationToken.Cancel(); 敘述是在 Task.Run 之後才執行,可是,不要以為就會直接進入到非同步工作的委派方法內,從上面的執行結果內容,可以看出,在 Task.Run 這個方法尚未啟動與執行非同步委派方法前,就已經偵測到權杖取消訊號了,因此,直接取消了此非同步作業。
會有這樣的結果,那是因為要啟動與執行非同步工作,需要一些時間,而 cancellationToken.Cancel(); 敘述緊接著 Task.Run 之後就直接執行,所以,這個敘述會先執行,之後非同步工作的運作環境準備好之後,需要開始執行時,就會檢查取消權杖訊號是否已經發送出,此時,就會看到這樣的執行結果輸出內容了。
+

情境四:執行 Task.Run 之後,等候 1 秒鐘,送出權杖取消訊號

  • 請將狀態2 底下的 Thread.Sleep(1000); 解除註解
  • 請將狀態3 底下的 cancellationToken.Cancel(); 解除註解
  • 請再度執行該專案
正在啟動非同步工作
按下任一按鍵,檢查工作狀態
非同步工作已經取消了
非同步工作結束了
 工作狀態 RanToCompletion
Press any key for continuing...
現在,非同步工作已經正常執行起來,接著才會發出取消權杖訊號,所以,當非同步工作委派方法執行之後的五秒鐘,在檢查取消權杖狀態的時候,就會終止非同步工作的作業

了解更多關於 [Task Class] 的使用方式


了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式






2019年8月3日 星期六

為何需要 .NET Standard

為何需要 .NET Standard

對於 .NET Standard 這樣的一個開發技術,相信對於大多數的開發者而言,應該會有很多的困惑,微軟為什麼需要提出這樣的技術呢?若身為開發者的你,僅僅在單一開發框架下進行專案開發,也許會對於這樣的技術感受不到任何該技術帶來的好處,但是,只要你使用 .NET 開發程式語言進行跨平台的專案開發的時候,就會知道這樣的開發技術所帶來的好處;就算你都在 .NET Framework 平台下來開發專案,使用 .NET Standard 技術,也會為你帶來相當的開發優勢與好處的。

了解更多關於 [.NET 標準 Standard




首先,.NET Standard 就有點像是 HTML 這樣的標準規格,只要你的瀏覽器有依據 HTML 規格進行開發,例如,你自製的瀏覽器有支援 HTML5,那麼,你開發的瀏覽器軟體就可以顯示出使用 HTML5 標準規格所設計的網頁內容。而 .NET Standard 也是相同,他是一個 .NET API 標準規格,在 .NET Standard 的不同版本內,定義了他支援了那些 .NET API與類別,你可以建立一個 .NET Standard 2.0 的類別庫,只要你所在的任何開發平台是 .NET Standard 2.0 所支援到的,那麼,就可以直接在該該平台專案下直接參考 Reference 這個 .NET Standard 2.0 類別庫組件,並且可以使用該類別庫內的相關類別與 API。
其實,.NET Standard 另外一個很重要的目的,那就是只要你所建立的類別庫,指定使用 .NET Standard 來進行設計,只要你選擇的 .NET Standard 版本所支援的 .NET 開發平台有支援,那麼,就可以直接在這些開發平台下,加入這個 .NET Standard 類別庫組件,便可以直接無誤的使用這個類別庫所設計的相關類別與 API。
有些人認為不使用 .NET Standard 所建立的類別庫,也可以在不同的開發平台下來進行參考與使用,其實這是會有問題的喔,接下來將會使用 .NET Core 2.2 / .NET Framework 4.7.2 / .NET Framework 4.0 開發平台做為測試範例。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

建立一個 .NET Core 2.2 類別庫

首先先來建立一個 .NET Core 2.2 類別庫,名為 ClassLibraryNETCore22。在這個類別庫專案內,將會建立一個 Class1 的類別,裡面僅有一個 DoSomething 方法,如底下程式碼所示。
這個 API File.WriteAllTextAsync() 將會在 .NET Core 2.2 下有支援,而且 在 .NET Core 2.2 下,可以有底下兩種同步與非同步的 WriteAllText 方法可以選擇 void WriteAllText(string path, string contents)與 Task WriteAllTextAsync(string path, string contents, CancellationToken cancellationToken = default);
C Sharp / C#
public class Class1
{
    public async Task DoSomething()
    {
        await File.WriteAllTextAsync("MyFile", "MyContent");
    }
}

建立一個 .NET Core 2.2 Console 專案,並且加入參考 .NET Core 2.2 類別庫

現在建立起一個 .NET Core 2.2 Console 專案,名為 ConsoleNETCore22,因此,這樣的使用方式是沒有問題的,也就是在該專案使用剛剛建立的類別庫內的類別,當然,這個 ClassLibraryNETCore22 類別庫是可以加入到 .NET Core 2.2 的專案裡面,並且建立起一個 Class1 類別的執行個體,使用執行個體方法 DoSomething 來呼叫,一切都是可以正常運作的。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        var foo = new ClassLibraryNETCore22.Class1().Wait();
        foo.DoSomething();
    }
}

建立一個 .NET Framework 4.7.2 Console 專案,並且加入參考 .NET Core 2.2 類別庫

既然現在有個 .NET Core 類別庫專案,或者說 .NET Core 的類別庫組件,很多人認為既然都是 .dll 檔案,當然是可以加入到任何 .NET 專案中來使用,現在,先來建立起一個 .NET Framework 4.7.2 Console 專案,名為 ConsoleNETFramework472,接著,在這個 Console 專案加入剛剛建立的 .NET Core 類別庫專案,看看會有甚麼情況發生。
一旦在 .NET Framework 4.7.2 的專案,加入參考 .NET Core 的類別庫專案,此時,將會看到底下的錯誤訊息
專案 '..\ClassLibraryNETCore22\ClassLibraryNETCore22.csproj' 以 'netcoreapp2.2' 為目標,無法供目標為 '.NETFramework,Version=v4.7.2' 的專案參考。    ConsoleNETFramework472
喔喔,竟然連建置這個程序都無法正常執行,直接噴出錯誤,雖然都是 .dll 組件檔案,不過,因為其開發框架的類型不相同,導致無法使用,所以,若想要使用 .NET Core 開發平台,建立起一系列的類別庫,讓其他 .NET 開發平台來使用,這樣的操作是行不同的。

建立一個 .NET Framework 4.7.2 類別庫

可是,若反過來建立一個 .NET Framework 4.7.2 類別庫專案,這個專案是否可以再 .NET Core 中順利使用呢?答案是不一定,因為在 .NET Framework 內的 API,有很多是在 .NET Core 開發平台下不支援的,或者在某些情境下會發生問題的。
現在,建立一個 .NET Framework 4.7.2 類別庫專案,名為 ClassLibraryNETFramework472 ,在這個專案內,同樣的有建立一個 Class1 類別,在這個裡面有兩個方法: DoSomething() , DoWebSomething() 。其中,DoSomething() 等下會進行測試,可以正常在 .NET Framework 4.7.2 平台下運行,可以,卻無法在 .NET Core 平台下運行(不過,可以正常建置與執行);而 DoWebSomething() 方法是用來在不同 .NET Framework 版本下用來測試,使用 .NET Framework 建立起來的類別庫,其相關 API 是否可以在不同的 .NET Framework 平台下來運行。
C Sharp / C#
public class Class1
{
    public void DoSomething()
    {
        File.WriteAllText("MyFile", "MyContent");
        var foo = System.Text.Encoding.GetEncoding(1252);
    }
    public void DoWebSomething()
    {
        // 在 .NET Framework 下,HttpClient 需要在 .NET Framework 4.5以上才有支援
        // void WriteAllText(string path, string contents);
        //
        HttpClient client = new HttpClient();
    }
}

.NET Framework 4.7.2 Console 專案,加入參考 .NET Framework 4.7.2 類別庫

請將剛剛建立好的 .NET Framework 4.7.2 專案 (ConsoleNETFramework472) ,確認該專案只有加入 .NET Framework 4.7.2 類別庫的專案 (ClassLibraryNETFramework472)。最後,在此 Console 專案內的 Main 方法內,加入底下的敘述。
現在,請建置與執行這個專案,這裡是可以正常運行無誤。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        var foo = new ClassLibraryNETFramework472.Class1();
        foo.DoSomething();
    }
}

.NET Core Console 專案,加入參考 .NET Framework 4.7.2 類別庫

現在,請在 .NET Core Console 專案內,加入參考 .NET Framework 4.7.2 類別庫 (ClassLibraryNETFramework472) ,並且在該 Console Main 方法內,修改成為底下的程式碼。
首先,請嘗試建置這個 .NET Core Console 專案,此時,可以順利正常建置成功
然後,現在開始執行這個 .NET Core Console 專案,不過,卻得到底下的錯誤訊息;所以,若想要建立 .NET Framework 的類別庫,想要讓其他平台專案下來使用,也是不可行的,特別要注意的是,許多 .NET Framework 的 API,在 .NET Core 是不支援的,可以參考 ApiCompat 有更多的說明資訊。
System.NotSupportedException: 'No data is available for encoding 1252. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.'
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        var foo = new ClassLibraryNETCore22.Class1();
        foo.DoSomething();
        var bar = new ClassLibraryNETFramework472.Class1();
        bar.DoSomething();
    }
}

建立一個 .NET Framework 4.0 Console 專案

現在,要來測試這個問題,若使用 .NET Framework 4.7.2 建立的類別庫專案,是否可以用於其他版本的 .NET Framework 專案下,例如,是否可以再 .NET Framework 4.0 Console 專案內呢?
現在請建立一個 .NET Framework 4.0 Console 專案,名為 ConsoleNETFramework40 ,並且將 .NET Framework 4.7.2 的類別庫 ( ClassLibraryNETFramework472 ) 加入參考到這個專案內。
請直接開始建置這個專案(雖然 Main 方法內還沒有加入其他程式碼),將會得到底下的警告訊息,不過,卻是可以正常建置成功的。
無法解析主要參考 "D:\Vulcan\GitHub\CSharp2019\WhyNeedNETStandard\ClassLibraryNETFramework472\bin\Debug\ClassLibraryNETFramework472.dll",因為它在架構組件 "System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" 上有間接相依性,但此組件無法在目前設為目標的架構中解析。".NETFramework,Version=v4.0"。若要解決此問題,請移除參考 "D:\Vulcan\GitHub\CSharp2019\WhyNeedNETStandard\ClassLibraryNETFramework472\bin\Debug\ClassLibraryNETFramework472.dll",或將應用程式的目標重定為包含 "System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" 的架構版本。    ConsoleNETFramework40            

參考的專案 'ClassLibraryNETFramework472' 目標設定的 Framework 版本 (4.7.2) 比這個專案的目前目標 Framework 版本 (4.0) 高。如果相依性鏈結中的任何專案使用這個專案的目標 Framework 以外的組件中的類型,可能導致組建失敗。    ConsoleNETFramework40            

無法解析主要參考 "D:\Vulcan\GitHub\CSharp2019\WhyNeedNETStandard\ClassLibraryNETFramework472\bin\Debug\ClassLibraryNETFramework472.dll",因為它是針對 ".NETFramework,Version=v4.7.2" 架構所建置。這個版本高於目前的目標架構 ".NETFramework,Version=v4.0"。    ConsoleNETFramework40
請修改 Main 方法的程式碼如下所示,接著,就可以再度建置這個專案,同樣的沒有發現到任何錯誤訊息,還是只有這3個警告訊息。
當要執行這個專案的時候,卻發生了底下的錯誤
從 Visual Studio 輸出視窗可以看到這個錯誤訊息
error CS0246: 找不到類型或命名空間名稱 'ClassLibraryNETFramework472' (是否遺漏了 using 指示詞或組件參考?)
所以,苦以驗證這樣的作法也是不行的
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        var class1 = new ClassLibraryNETFramework472.Class1();
        class1.DoWebSomething();
    }
}

結論

想要設計一系列的類別庫,並且可以讓這些類別庫在不同 .NET 平台下來使用,最好的做法就是建立一個 .NET Standard 類別庫

了解更多關於 [.NET 標準 Standard