線程階段性總結——APM,ThreadPool,Task,TaskScheduler ,CancellationTokenSource


不管我們使用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;
        }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM