不管我們使用thread,threadPool,task,還是APM異步,本質都是在使用多線程。對於新手來說,不太敢用多線程的原因,就我個人的體驗來說,就是對多線程的異常捕獲方式或時機缺乏了解,而一旦出現異常沒有捕獲,將會帶來難以發現的bug,進而造成系統崩潰。而多線程本身也不是一朝一夕就能學好的,必須不斷的去學習總結,所以我個人認為你要用一種線程模型,首先要對它有足夠的了解,特別是對異常的捕獲。如果你沒有完全的把握,最好在實際開發中謹慎的用多線程。
1,APM異步編程模型。
采用BeginXXX和EndXXX方法。關於異常的捕捉,對於剛調用BeginXXX拋出的異常,異步操作可能還沒有進入隊列。這種異常一般可以忽略。對於進入異步操作時發生的異常,會將錯誤碼放入IAsyncResult對象中,在我們調用EndXXX方法時,會將這個錯誤碼轉換成一個恰當的Exception再次拋出。所以對於APM編程模型來說,我們只用對EndXXX方法進行異常捕捉。偽代碼:
Try { Result = someObj.EndXXX(IAsyncResult); } Catch(xxxException e) { //異常處理 }
注意事項:
1) 對於EndXXX方法的調用是必須的,否則可能會造成資源的泄漏,即使你可能不關心異步調用的返回結果,也要記住調用這個方法。
2) 只能調用一次EndXXX方法。
3) 調用EndXXX方法總是使用和BeginXXX時相同的對象。這里辨別的是引用,引用不同就被視為不同的對象。對於Delegate要補充一點,即使是相同簽名的委托,它們被編譯器編譯成具體的類,這些類的類名是不一樣的。
4) 不能取消異步I/O限制的異步操作。不要迷信這句話,他說的是I/O操作,是指的一個請求動作,如果我們的是多次請求,比如異步分塊上傳文件,是可以做取消功能的。
5) FCL中有許多的I/O操作類都實現了APM。如派生自System.IO.Stream的類,Socket,Dns,WebRequest,還有SqlCommand等等。它們都提供了BeginXXX和EndXXX方法。
6) 可以用APM來執行任何方法,我們只需要定義一個與方法簽名一致的delegate,delegate編譯后會生成一個BeginInvoke和EndInvoke方法來支持APM操作。
2,Thread & ThreadPool
Thread和ThreadPool發起異步的缺點:
1)沒有內建的機制知道任務何時完成。
2)沒法得到任務的返回值。
Thread的開銷太大,盡量用ThreadPool,除非你要顯示指定你的thread為前台線程或要對線程設置優先級,否則就不要用thread。
注意:線程池是由所有的AppDomain共享的。一個CLR維持一個線程池。
3,Task
Task的引入,解決了上面的兩個問題。
1)Task可通過Wait()方法來等待任務的完成。這個方法是阻塞的。
2)通過Task.Result可以得到返回結果。在Result內部調用了Wait方法,所以查詢這個屬性是阻塞的。
3)對於任務函數的未處理異常,會被包裝成AggregateException異常拋出。可捕捉Wait()方法和Result屬性。通過AggregateException的InnerExceptions可以進一步查詢具體的異常。
4)Task的靜態方法WaitAny和WaitAll可以等待多個任務返回。同樣可以捕捉這兩個方法的異常。
5)對於沒有調用Wait,Result,Exception來查詢未處理異常的情況,例如:只調用了Task.Start方法。Task對象被回收時,Finalize方法會再次拋出這個異常終止進程。可以向TaskScheduler.UnobservedTaskException事件注冊一個方法,來處理這類異常。通過UnobservedTaskExceptionEventArgs的SetObserved方法,可以忽略掉這個異常,使進程不會終止。
6)構造Task時,可以傳遞CancellationToken對象,以支持取消。如果是任務函數,通過調用CancellationToken.ThrowIfCancellationRequested 拋出的異常,類型是OperationCanceledException。如果任務函數沒有傳遞CancellationToken對象,那么拋出的異常是TaskCanceledException,相當於任務級別的取消。
7) Task的ContinueWith方法可以在第一個任務完成時開啟第二個任務。這個功能很強大,ContinueWith方法並不阻塞調用線程,它是異步的。我們可以在ContinueWith中寫一個事件回調方法,它可以起到事件完成通知的作用。但它只能收到任務完成的通知,要實現任務進度的更新通知,到目前為止,task依然做不到。
Task<int> t = new Task<int>(() => Sum(100)); t.Start(); t.ContinueWith(task => Console.WriteLine("result:" + task.Result), TaskContinuationOptions.OnlyOnRanToCompletion); t.ContinueWith(task => Console.WriteLine("canceled"), TaskContinuationOptions.OnlyOnCanceled); t.ContinueWith(task => Console.WriteLine("failed"), TaskContinuationOptions.OnlyOnFaulted);
對於上面的這一竄代碼,如果有未處理異常,同樣會造成進程終止。你同樣可以用TaskScheduler.UnobservedTaskException事件注冊一個方法來處理。
8)Task可以指定子任務,子任務沒有完成,父任務的ContinueTask也不會執行。關於異常和上面的處理方法一樣。因為這個也是不阻塞的,未處理異常暫時也只能在TaskScheduler.UnobservedTaskException里處理。
Task<Int32[]> parent = new Task<int[]>(() => { Int32[] result = new Int32[3]; new Task<Int32>(() => result[0] = Sum(100), TaskCreationOptions.AttachedToParent).Start(); new Task<Int32>(() => result[1] = Sum(200), TaskCreationOptions.AttachedToParent).Start(); new Task<Int32>(() => result[2] = Sum(300), TaskCreationOptions.AttachedToParent).Start(); return result; }); parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, num => Console.WriteLine(num))); parent.Start();
9)TaskFactroy可以簡化一組相似Task的創建工作。
Task parent = new Task(() => { CancellationTokenSource cts = new CancellationTokenSource(); TaskFactory<Int32> tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); //create three child task var childTasks = new[]{ tf.StartNew(()=>Sum(cts.Token,100)), tf.StartNew(()=>Sum(cts.Token,200)), tf.StartNew(()=>Sum(cts.Token,Int32.MaxValue)) }; //when one failed,cancel the other for (int i = 0; i < childTasks.Length; i++) { childTasks[i].ContinueWith(task => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted); } //display the maxvalue tf.ContinueWhenAll( childTasks, completeTasks => completeTasks.Where( task => !task.IsCanceled && !task.IsFaulted).Max(t => t.Result), CancellationToken.None).ContinueWith(task => Console.WriteLine("the max is:" + task.Result)); }); //show exception parent.ContinueWith(p => { StringBuilder sb = new StringBuilder(); sb.AppendLine("error occours:"); foreach (var e in p.Exception.Flatten().InnerExceptions) { sb.AppendLine(e.Message); } Console.WriteLine(sb.ToString()); }, TaskContinuationOptions.OnlyOnFaulted); parent.Start();
4,對於協作取消要用CancellationTokenSource類。
1)CancellationTokenSource.Token方法返回CancellationToken,可以將CancellationToken傳入我們的工作方法,並查詢CancellationToken.IsCancellationRequested屬性來獲得操作是否已取消。取消的情況下,可以結束工作方法。
2)一般在主線程調用取消方法。CancellationToeknSource.Cancel。
3)取消時可以加入回調方法,通過CancellationToken.Register方法注冊。對於回調方法拋出的異常,可以捕捉Cancel方法,異常會被包裝到AggregateException異常中,查詢InnerExceptions可的異常的詳細信息。
4)CancellationTokenSource的靜態方法CreateLinkedTokenSource可以創建一個關聯的CreateLinkedTokenSource對象。任意其中的一個CreateLinkedTokenSource被取消,這個關聯的CreateLinkedTokenSource就會被取消。
5,任務調度器。
分為線程池任務調度器(thread pool task scheduler)和同步上下文任務調度器(synchroliazation context task scheduler)。其中同步上下文任務調度器能將所有的任務調度給UI線程,這對於更新界面的異步操作相當有用!默認的調度器是線程池任務調度器。
非UI線程更新UI界面會報錯,可以用下面的方法,指定同步上下文任務調度器:
TaskScheduler syncSch = TaskScheduler.FromCurrentSynchronizationContext(); Task<int> t = new Task<int>(() => Sum(100)); //update UI with Synchronizationcontext t.ContinueWith(task => Text = task.Result.ToString(), syncSch); t.Start();
6,非UI線程更新UI界面的方式總結
詳見我的另一篇文章:
http://www.cnblogs.com/xiashengwang/archive/2012/08/18/2645541.html
7,Parallel
這個類提供了For,Foreach,Invoke靜態方法。它內部封裝了Task類。主要用於並行計算。
private void ParallelTest2() { for (int i = 1; i < 5; i++) { Console.WriteLine(DoWork(i)); } //和上面的代碼等價,但是是多線程並行執行的,注意這里的結束index不包含5 var plr = Parallel.For(1, 5, i => Console.WriteLine(DoWork(i))); } private int DoWork(int num) { int sum = 0; for (int i = 0; i <= num; i++) { sum += i; } return sum; }