帶着問題去思考!大家好!
簡介
之前我們說了線程池和線程以及運用。實際上可以理解爲他只是一個抽象層,其向程序員隱藏了使用線程的細節。但是使用線程池也是相當複雜,接着我們運用異步編程模型和基於事件的異步模式,這樣獲取結果很容易,傳播異常也很輕鬆,但是組合多個異步操作仍然需要大量的工作,所以在.NET Framework 4.5引入了一個新的異步操作的API,任務並行庫(Task).隨之下一個版本進行更新。
APM API轉換爲任務
首先我們要知道什麼是APM模式。
.net 1.0時期就提出的一種異步模式,並且基於IAsyncResult接口實現BeginXXX和EndXXX類似的方法。
//APM API模式轉換爲任務.Net Core不支持 private delegate string AsynchronousTask(string threadName); private delegate string IncompatibleAsynchronousTask(out int threadId); /// <summary> /// 回調 /// </summary> /// <param name="ar"></param> private static void Callback(IAsyncResult ar) { Console.WriteLine("開始一個回調..."); Console.WriteLine("傳遞給回調的狀態:{0} ", ar.AsyncState); Console.WriteLine("是否是線程池線程:{0}", Thread.CurrentThread.IsThreadPoolThread); Console.WriteLine("線程池任務的線程id:{0}", Thread.CurrentThread.ManagedThreadId); } private static string Test(string threadName) { Console.WriteLine("開始....."); Console.WriteLine("是否是線程池線程:{0}", Thread.CurrentThread.IsThreadPoolThread); Thread.Sleep(TimeSpan.FromSeconds(2)); Thread.CurrentThread.Name = threadName; return string.Format("線程名稱:{0}", Thread.CurrentThread.Name); } private static string Test(out int threadId) { Console.WriteLine("開始....."); Console.WriteLine("是否是線程池線程:{0}", Thread.CurrentThread.IsThreadPoolThread); Thread.Sleep(TimeSpan.FromSeconds(2)); threadId = Thread.CurrentThread.ManagedThreadId; return string.Format("線程池任務線程id是:{0}", threadId); } static void Main(string[] args) { int threadId; //委託賦值 AsynchronousTask d = Test; IncompatibleAsynchronousTask e = Test; Console.WriteLine("選擇1"); Task<string> task = Task<string>.Factory.FromAsync(d.BeginInvoke("AsyncTaskThread", Callback, "a delegate asynchronous call"), d.EndInvoke); task.ContinueWith(t => Console.WriteLine("回調已經完成, now running a continuation! Result:{0}", t.Result)); while (!task.IsCompleted) { Console.WriteLine(task.Status); Thread.Sleep(TimeSpan.FromSeconds(0.5)); } Console.WriteLine(task.Status); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("----------------------"); Console.WriteLine("選擇2"); task = Task<string>.Factory.FromAsync(d.BeginInvoke, d.EndInvoke, "AsyncTaskThread", "a delegate asynchronous call"); task.ContinueWith(t => Console.WriteLine("任務已經完成, now running a continuation! Result:{0}", t.Result)); while (!task.IsCompleted) { Console.WriteLine(task.Status); Thread.Sleep(TimeSpan.FromSeconds(0.5)); } Console.WriteLine(task.Status); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("----------------------"); Console.WriteLine("選擇3"); IAsyncResult ar = e.BeginInvoke(out threadId, Callback, "a delegate asynchronous call"); ar = e.BeginInvoke(out threadId, Callback, "a delegate asynchronous call"); task = Task<string>.Factory.FromAsync(ar, _ => e.EndInvoke(out threadId, ar)); task.ContinueWith(t => Console.WriteLine("任務已經完成, now running a continuation! Result:{0}", t.Result)); while (!task.IsCompleted) { Console.WriteLine(task.Status); Thread.Sleep(TimeSpan.FromSeconds(0.5)); } Console.WriteLine(task.Status); Thread.Sleep(TimeSpan.FromSeconds(1)); }
這裏我們定義兩個委託,其中一個使用了OUT參數,因此再將APM模式轉換爲任務時,與標準的TPL API是不兼容的。
將APM轉換爲TPL的關鍵點是Task<T>.Factory.FromAsync方法,T是異步操作結果是類型。該方法有數個重載。在第一例子中傳入了IAsyncResult和Func<IAsyncResult,string>,這是一個將IAsyncResult的實現參數並返回一個字符串的方法。由於第一個委託提供的EndMethod與該簽名是兼容的,所以將該委託的異步調用轉換爲任務是可以的。
第二個例子與第一個非常相似,使用了不同的FromAsync方法重載,該重載並不允許指定一個將會在異步委託調用完成後被調用的回調函數,但我們可以使用後續操作替代它,但如果回調函數很重要,可以使用第一個例子的方法。
最後一個例子展示了一個小技巧,這次IncompatibleAsynchronousTask委託的EndMethod使用了out參數。與FromAsync方法並不兼容。然而,可以很容易的將EndMethod調用封裝到一個lambda表達式中,從而適合任務工廠方法。
第一個任務狀態爲:WaitingForActivation,這意味TPL基礎設施實際上還爲啓動任務
實現取消
基於任務的異步操作實現取消流程。我們將學習如果正確的使用取消標誌,以及在任務真正運行前如何得知其他是否被取消。
/// <summary> /// 任務取消 /// </summary> /// <param name="name"></param> /// <param name="seconds"></param> /// <param name="token"></param> /// <returns></returns> private static int TestMethod(string name,int seconds,CancellationToken token) { Console.WriteLine("task {0} is running on a thread id {1},Is thread pool thread {2}",name,Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); for (int i = 0; i < seconds; i++) { Thread.Sleep(TimeSpan.FromSeconds(1)); if (token.IsCancellationRequested) return -1; } return 42 * seconds; } static void Main(string[] args) { var cts =new CancellationTokenSource(); var longTask = new Task<int>(()=>TestMethod("task 1",10,cts.Token),cts.Token); Console.WriteLine(longTask.Status); cts.Cancel(); Console.WriteLine(longTask.Status); Console.WriteLine("First task has been cancelled before execution"); cts = new CancellationTokenSource(); longTask = new Task<int>(() => TestMethod("task 2", 10, cts.Token), cts.Token); longTask.Start(); for (int i = 0; i < 5; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine(longTask.Status); } cts.Cancel(); for (int i = 0; i < 5; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine(longTask.Status); } Console.WriteLine("A task has been completed with result {0}.",longTask.Result); }
這裏我們所以說的爲TPL任務實現取消選擇的例子
首先仔細看看longTask的創建代碼。我們將底層任務傳遞一次取消標誌,然後給任務構造函數再傳遞一次,爲什麼需要提供取消標誌兩次呢?
是這樣的;如果在任務實際啓動前取消它,該任務的TPL基礎設施有責任處理該取消操作。因爲這些代碼根本不會執行,通過得到的第一個任務的狀態可以知道它被取消了。如果嘗試堆該任務調用Start方法,將會得到InvalidOperationException異常。
然後需要自己寫代碼來處理取消過程,這意味着我們對取消過程全權負責,並且在取消任務後,任務的狀態仍然是RanToCompletion,因爲從TPL的視角來看,該任務正常完成了它的工作。辨別這兩種情況是很重要的,並且需要理解每種情況下職責的不同。
處理任務中的異常
拋出異常的不同情況
/// <summary> /// 任務異常 /// </summary> /// <param name="name"></param> /// <param name="seconds"></param> /// <returns></returns> public static int TaskMethod(string name,int seconds) { Console.WriteLine("task {0} is running on a thread id {1},Is thread pool thread {2}", name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); Thread.Sleep(TimeSpan.FromSeconds(seconds)); throw new Exception("Boom"); return 42 * seconds; } static void Main(string[] args) { Task<int> task; try { task =Task.Run(() => TaskMethod("Task 1", 2)); int result = task.Result; Console.WriteLine("Result:{0}",result); } catch (Exception ex) { Console.WriteLine("Exception caught:{0}", ex); } Console.WriteLine("---------------------"); Console.WriteLine(); try { task = Task.Run(() => TaskMethod("Task 2", 2)); int result = task.GetAwaiter().GetResult(); Console.WriteLine("Result:{0}", result); } catch (Exception ex) { Console.WriteLine("Exception caught:{0}", ex); } Console.WriteLine("---------------------"); Console.WriteLine(); var t1 = new Task<int>(()=>TaskMethod("Task 3",3)); var t2 = new Task<int>(()=>TaskMethod("Task 4",2)); var complexTask = Task.WhenAll(t1, t2); var exceptionHandler = complexTask.ContinueWith(t => Console.WriteLine("Exception caught:{0}", t.Exception, TaskContinuationOptions.OnlyOnFaulted)); t1.Start(); t2.Start(); Thread.Sleep(TimeSpan.FromSeconds(5)); }
當程序啓動時,創建一個任務並嘗試同步獲取任務結果。Result屬性的Get部分會使當前線程等待直到該任務完成,並將異常傳播給當前線程。這種情況下,通過catch代碼塊可以很容易的捕獲異常,但是異常是一個被封裝的異常。叫做AggregateException。在這個例子中,它裏面包含一個異常,因爲只有一個任務拋出異常。可以訪問InnerException屬性來得到底層異常。
第二個例子與第一個例子相似,不同的是使用了GetAwaiter和GetResult方法來訪問任務結果。這種情況下,無需封裝異常,因爲TPL基礎設施會提取該異常。如果只有一個底層任務,那麼一次只能獲取一個原始異常,這種設計非常合適
最後一個例子展示了兩個任務拋出異常的情形,現在使用後續操作來處理異常。只有之前的任務完成前有異常時,該後續操作纔會被執行。通過給後續操作傳遞TaskContinuationOptions.OnlyOnFaulted選項可以實現該行爲。