Task101.txt ---------- 20200302 https://blog.darkthread.net/blog/net4-task/ 簡介.NET 4.0的多工執行利器--Task 前陣子試寫SignalR時,學到.NET 4.0在多工執行上提供了新類別--Task。初試之下,發現用它取代傳統Thread、ThreadPool寫法,能大幅簡化同步邏輯的寫法,頗為便利。整理幾個範例展示Task的使用方式,分享兼備忘。 先從最簡單的開始。test1()用以另一條Thread執行Thread.Sleep()及Console.WriteLine(),效果與ThreadPool.QueueUserWorkItem()相當。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Threading; using System.Diagnostics; namespace TaskLab { class Program { static void Main(string[] args) { test1(); Console.Read(); } static void test1() { //Task可以代替TheadPool.QueueUserWorkItem使用 Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("Done!"); }); Console.WriteLine("Async Run..."); } } } StartNew()完會立刻執行下一行,故會先看到Aync Run,1秒後印出Done。 Async Run... Done! 同時啟動數個作業多工並行,但要等待各作業完成再繼續下一步是常見的應用情境,傳統上可透過WaitHandle、AutoResetEvent、ManualResetEvent等機制實現;Task的寫法相對簡單,建立多個Task物件,再當成Task.WaitAny()或Task.WaitAll()的參數就搞定囉! static void test2() { var task1 = Task.Factory.StartNew(() => { Thread.Sleep(3000); Console.WriteLine("Done!(3s)"); }); var task2 = Task.Factory.StartNew(() => { Thread.Sleep(5000); Console.WriteLine("Done!(5s)"); }); //等待任一作業完成後繼續 Task.WaitAny(task1, task2); Console.WriteLine("WaitAny Passed"); //等待兩項作業都完成才會繼續執行 Task.WaitAll(task1, task2); Console.WriteLine("WaitAll Passed"); } task1耗時3秒、task2耗時5秒,所以3秒後WaitAny()執行完成、5秒後WaitAll()執行完畢。 Done!(3s) WaitAny Passed Done!(5s) WaitAll Passed 如果要等待多工作業傳回結果,透過StartNew()指定傳回型別建立作業,隨後以Task.Result取值,不用額外寫Code就能確保多工作業執行完成後才讀取結果繼續運算。 static void test3() { var task = Task.Factory.StartNew(() => { Thread.Sleep(2000); return "Done!"; }); //使用馬錶計時 Stopwatch sw = new Stopwatch(); sw.Start(); //讀task.Result時,會等到作業完畢傳回值後才繼續 Console.WriteLine("{0}", task.Result); sw.Stop(); //要取得task.Result耗時約2秒 Console.WriteLine("Duration: {0:N0}ms", sw.ElapsedMilliseconds); } 實際執行,要花兩秒才能跑完Console.WriteLine("{0}", task.Result),其長度就是Task執行並傳回結果的時間。 Done! Duration: 2,046ms 如果要安排多工作業完成後接連執行另一段程式,可使用ContinueWith(): static void test4() { Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("Done!"); }).ContinueWith(task => { //ContinueWith會等待前項工作完成才執行 Console.WriteLine("In ContinueWith"); }); Console.WriteLine("Async Run..."); } 如預期,ContinueWith()裡的程式會在Task完成後才被執行。 Async Run... Done! In ContinueWith .ContinueWith()傳回值仍是Task物件,所以我們可以跟jQuery一樣玩接接樂,在ContinueWith()後方再接上另一個ContinueWith(),各段邏輯便會依順序執行。 static void test5() { //ContinueWith()可以串接 Task.Factory.StartNew(() => { Thread.Sleep(2000); Console.WriteLine("{0:mm:ss}-Done", DateTime.Now); }) .ContinueWith(task => { Console.WriteLine("{0:mm:ss}-ContinueWith 1", DateTime.Now); Thread.Sleep(2000); }) .ContinueWith(task => { Console.WriteLine("{0:mm:ss}-ContinueWith 2", DateTime.Now); }); Console.WriteLine("{0:mm:ss}-Async Run...", DateTime.Now); } Task耗時兩秒,第一個ContinueWith()耗時2秒,最後一個ContinueWith()接續在4秒後執行。 59:13-Async Run... 59:15-Done 59:15-ContinueWith 1 59:17-ContinueWith 2 最後一個例子比較複雜。ContinueWith()中的Action都會有一個輸入參數,藉以得知前一Task的執行狀態,有IsCompleted, IsCanceled, IsFaulted幾個屬性可用。 要取消執行,得借助CancellationTokenSource及其所屬CancellationToken類別,做法是在Task中持續呼叫CancellationToken.ThrowIfCancellationRequested(),一旦外部呼叫CancellationTokenSource.Cancel(),便會觸發OperationCanceledException,Task有機制偵測此種例外狀況,將結束作業執行後續的ContinueWith(),並指定Task.IsCanceled為True以為識別;而當Task程式發生Exception,也會結束作業觸發ContinueWith(),此時則Task.IsFaulted為True,ContinueWith()中可透過Task.Exception.InnerExceptions取得錯誤細節。 以下程式同時可測試Task正常、取消及錯誤三種情境,使用者透過輸入1,2或3來決定要測試哪一種。在Task外先宣告一個CancellationTokenSource類別,將其中的Token屬性當成StartNew()的第二項參數,而Task中則保留最初的五秒可以取消,方法是每隔一秒呼叫一次CancellationToken.ThrowIfCancellationRequested(),當程式外部呼叫CancellationTokenSource.Cancel(),Task就會結束。5秒後若未取消,再依使用者決定的測試情境return結果或是抛出Exception。ContinueWith()則會檢查IsCanceled, IsFaulted等旗標,並輸出結果。 static void test6() { CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken cancelToken = cts.Token; Console.Write("Test Option 1, 2 or 3 (1-Complete / 2-Cancel / 3-Fault) : "); var key = Console.ReadKey(); Console.WriteLine(); Task.Factory.StartNew(() => { //保留5秒偵測是否要Cancel for (var i = 0; i < 5; i++) { Thread.Sleep(1000); //如cancelToken.IsCancellationRequested //抛出OperationCanceledException cancelToken.ThrowIfCancellationRequested(); } switch (key.Key) { case ConsoleKey.D1: //選1時 return "OK"; case ConsoleKey.D3: //選2時 throw new ApplicationException("MyException"); } return "Unknown Input"; }, cancelToken).ContinueWith(task => { Console.WriteLine("IsCompleted: {0} IsCanceled: {1} IsFaulted: {2}", task.IsCompleted, task.IsCanceled, task.IsFaulted); if (task.IsCanceled) { Console.WriteLine("Canceled!"); } else if (task.IsFaulted) { Console.WriteLine("Faulted!"); foreach (Exception e in task.Exception.Flattern().InnerExceptions) { Console.WriteLine("Error: {0}", e.Message); } } else if (task.IsCompleted) { Console.WriteLine("Completed! Result={0}", task.Result); } }); Console.WriteLine("Async Run..."); //如果要測Cancel,2秒後觸發CancellationTokenSource.Cancel if (key.Key == ConsoleKey.D2) { Thread.Sleep(2000); cts.Cancel(); } } 以下是三種測試情境的結果。 正常執行: Test Option 1, 2 or 3 (1-Complete / 2-Cancel / 3-Fault) : 1 Async Run... IsCompleted: True IsCanceled: False IsFaulted: False Completed! Result=OK 取消: (IsCanceled為True,但留意IsCompleted也算True) Test Option 1, 2 or 3 (1-Complete / 2-Cancel / 3-Fault) : 2 Async Run... IsCompleted: True IsCanceled: True IsFaulted: False Canceled! 錯誤: (IsFaulted為True,IsCompleted也是True) Test Option 1, 2 or 3 (1-Complete / 2-Cancel / 3-Fault) : 3 Async Run... IsCompleted: True IsCanceled: False IsFaulted: True Faulted! Error: MyException 【小結】 說穿了,Task能做的事,過去使用Thread/ThreadPool配合Event、WaitHandle一樣能辦到,但使用Task能以較簡潔的語法完成相同工作,使用.NET 4.0開發多工作業程式應可多加利用。同時,Task也是.NET 4.5 async await的基礎概念之一,值得大家花點時間熟悉,有益無害。