任務(task)


任務概述

線程(Thread是創建並發的底層工具,因此有一定的局限性(不易得到返回值(必須通過創建共享域);異常的捕獲和處理也麻煩;同時線程執行完畢后無法再次開啟該線程),這些局限性會降低性能同時影響並發性的實現(不容易組合較小的並發操作實現較大的並發操作,會增加手工同步處理(加鎖,發送信號)的依賴,容易出現問題)。

線程池的(ThreadPoolQueueUserWorkItem方法很容發起一次異步的計算限制操作。但這個技術同樣有着許多限制,最大的問題是沒有內建的機制讓你知道操作在什么時候完成,也沒有機制在操作完成時獲得返回值。

Task類可以解決上述所有的問題。

任務(Task表示一個通過或不通過線程實現的並發操作,任務是可組合的,使用延續(continuation)可將它們串聯在一起,它們可以使用線程池減少啟動延遲,可使用回調方法避免多個線程同時等待I/O密集操作。

 

基礎任務(Task)

微軟在.NET 4.0 引入任務(Task的概念。通過System.Threading.Tasks命名空間使用任務。它是在ThreadPool的基礎上進行封裝的。Task默認都是使用池化線程,它們都是后台線程,這意味着主線程結束時其它任務也會隨之停止。

啟動一個任務有多種方式,如以下示例:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Console.WriteLine("主線程Id:{0}", Thread.CurrentThread.ManagedThreadId);
 6             int workerThreadsCount, completionPortThreadsCount;
 7             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
 8             Console.WriteLine("剩余工作線程數:{0},剩余IO線程數{1}", workerThreadsCount, completionPortThreadsCount);
 9             //第一種:實例化方式Start啟動
10             {
11                 Task task = new Task(() =>
12                 {
13                     Test("one-ok");
14                 });
15                 task.Start();
16             }
17             //第二種:通過Task類靜態方法Run方式進行啟動
18             {
19                 Task.Run(() =>
20                 {
21                     Test("two-ok");
22                 });
23             }
24             //第三種:通過TaskFactory的StartNew方法啟動
25             {
26                 TaskFactory taskFactory = new TaskFactory();
27                 taskFactory.StartNew(() =>
28                 {
29                     Test("three-ok");
30                 });
31             }
32             //第四種:.通過Task.Factory進行啟動
33             {
34                 Task taskStarNew = Task.Factory.StartNew(() =>
35                 {
36                     Test("four-ok");
37                 });
38             }
39             //第五種:通過Task對象的RunSynchronously方法啟動(同步,由主線程執行,會卡主線程)
40             {
41                 Task taskRunSync = new Task(() =>
42                 {
43                     Console.WriteLine("線程Id:{0},執行方法:five-ok", Thread.CurrentThread.ManagedThreadId);
44                 });
45                 taskRunSync.RunSynchronously();
46             }
47             Thread.Sleep(1000);
48             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
49             Console.WriteLine("剩余工作線程數:{0},剩余IO線程數{1}", workerThreadsCount, completionPortThreadsCount);
50             Console.ReadKey();
51         }
52         static void Test(string o)
53         {
54             Thread.Sleep(2000);
55             Console.WriteLine("線程Id:{0},執行方法:{1}", Thread.CurrentThread.ManagedThreadId, o);
56         }
57         /*
58          * 作者:Jonins
59          * 出處:http://www.cnblogs.com/jonins/
60          */
61     }

執行結果:

上面示例中除去使用RunSynchronously方法啟動的是同步任務(由啟用的線程執行任務)外,其它幾種方式內部都由線程池內的工作者線程處理。

說明

1.事實上Task.Factory類型本身就是TaskFactory(任務工廠),而Task.Run(在.NET4.5引入,4.0版本調用的是后者)是Task.Factory.StartNew的簡寫法,是后者的重載版本,更靈活簡單些。

2.調用靜態Run方法會自動創建Task對象並立即調用Start

3.如Task.Run等方式啟動任務並沒有調用Start,因為它創建的是“熱”任務,相反“冷”任務的創建是通過Task構造函數。

 

返回值(Task<TResult>)&狀態(Status)

Task有一個泛型子類Task<TResult>,它允許任務返回一個值。調用Task.Run,傳入一個Func<Tresult>代理或兼容的Lambda表達式,然后查詢Result屬性獲得結果。如果任務沒有完成,那么訪問Result屬性會阻塞當前線程,直至任務完成

1     public static Task<TResult> Run<TResult>(Func<TResult> function);

而任務的Status屬性可用於跟蹤任務的執行狀態,如下所示:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Task<int> task = Task.Run(() =>
 6             {
 7                 int total = 0;
 8                 for (int i = 0; i <= 100; i++)
 9                 {
10                     total += i;
11                 }
12                 Thread.Sleep(2000);
13                 return total;
14             });
15             Console.WriteLine("任務狀態:{0}",task.Status);
16             Thread.Sleep(1000);
17             Console.WriteLine("任務狀態:{0}", task.Status);
18             int totalCount = task.Result;//如果任務沒有完成,則阻塞
19             Console.WriteLine("任務狀態:{0}", task.Status);
20             Console.WriteLine("總數為:{0}",totalCount);
21             Console.ReadKey();
22         }
23     }

執行如下:

 

Reulst屬性內部會調用Wait(等待);

任務的Status屬性是一個TaskStatus枚舉類型:

1  public TaskStatus Status { get; }

說明如下:

枚舉值 說明
Canceled

任務已通過對其自身的 CancellationToken 引發 OperationCanceledException 對取消進行了確認,此時該標記處於已發送信號狀態;

或者在該任務開始執行之前,已向該任務的 CancellationToken 發出了信號。

Created 該任務已初始化,但尚未被計划。
Faulted 由於未處理異常的原因而完成的任務。
RanToCompletion 已完成執行的任務。
Running 任務正在運行,尚未完成。
WaitingForActivation 該任務正在等待 .NET Framework 基礎結構在內部將其激活並進行計划。
WaitingForChildrenToComplete 該任務已完成執行,正在隱式等待附加的子任務完成。
WaitingToRun 該任務已被計划執行,但尚未開始執行。

 

任務集合返回值(WhenAll&WhenAny)

 Task中有非常方便的對並行運行的任務集合獲取返回值的方式,比如WhenAllWhenAny

1.WhenAll

WhenAll等待提供的所有 Task 對象完成執行過程(所有任務全部完成)。

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             List<Task<int>> taskList = new List<Task<int>>();//聲明一個任務集合
 6             TaskFactory taskFactory = new TaskFactory();
 7             for (int i = 0; i < 5; i++)
 8             {
 9                 int total = i;
10                 Task<int> task = taskFactory.StartNew(() => Test(total));
11                 taskList.Add(task);//將任務放進集合中
12             }
13             Console.WriteLine("主線程Id:{0},繼續執行A.....", Thread.CurrentThread.ManagedThreadId);
14             Task<int[]> taskReulstList = Task.WhenAll(taskList);//創建一個任務,該任務將集合中的所有 Task 對象都完成時完成
15             for (int i = 0; i < taskReulstList.Result.Length; i++)//這里調用了Result,所以會阻塞線程,等待集合內所有任務全部完成
16             {
17                 Console.WriteLine("返回值:{0}", taskReulstList.Result[i]);//遍歷任務集合內Task返回的值
18             }
19             Console.WriteLine("主線程Id:{0},繼續執行B.....", Thread.CurrentThread.ManagedThreadId);
20             Console.ReadKey();
21         }
22         private static int Test(int o)
23         {
24             Console.WriteLine("線程Id:{0},Task執行成功,參數為:{1}", Thread.CurrentThread.ManagedThreadId, o);
25             Thread.Sleep(500 * o);
26             return o;
27         }
28     }

執行結果:

2.WhenAny

WhenAny:等待提供的任一 Task 對象完成執行過程(只要有一個任務完成)。

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             List<Task<int>> taskList = new List<Task<int>>();//聲明一個任務集合
 6             TaskFactory taskFactory = new TaskFactory();
 7             for (int i = 0; i < 5; i++)
 8             {
 9                 int total = i;
10                 Task<int> task = taskFactory.StartNew(() => Test(total));
11                 taskList.Add(task);//將任務放進集合中
12             }
13             Console.WriteLine("主線程Id:{0},繼續執行A.....", Thread.CurrentThread.ManagedThreadId);
14             Task<Task<int>> taskReulstList = Task.WhenAny(taskList);//創建一個任務,該任務將在集合中的任意 Task 對象完成時完成
15             Console.WriteLine("返回值:{0}", taskReulstList.Result.Result);//得到任務集合內最先完成的任務的返回值
16             Console.WriteLine("主線程Id:{0},繼續執行B.....", Thread.CurrentThread.ManagedThreadId);
17             Console.ReadKey();
18         }
19         private static int Test(int o)
20         {
21             Console.WriteLine("線程Id:{0},Task執行成功,參數為:{1}", Thread.CurrentThread.ManagedThreadId, o);
22             Thread.Sleep(500 * o);
23             return o;
24         }
25     }

執行結果(這里返回值肯定會是0,因為休眠最短):

 

等待(Wait)&執行方式(TaskCreationOptions)

1.任務等待(Wait)

調用任務的Wait方法可以阻塞任務直至任務完成,類似於線程的join

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Task task = Task.Run(() =>
 6             {
 7                 Console.WriteLine("線程執行Begin");
 8                 Thread.Sleep(2000);
 9                 Console.WriteLine("線程執行End");
10             });
11             Console.WriteLine("任務是否完成:{0}", task.IsCompleted);
12             task.Wait();//阻塞,直至任務完成
13             Console.WriteLine("任務是否完成:{0}", task.IsCompleted);
14             Console.ReadKey();
15         }
16     }

執行如下:

注意

線程調用Wait方法時,系統檢測線程要等待的Task是否已經開始執行。如果是線程則會阻塞直到Task運行結束為止。但如果Task還沒有開始執行任務,系統可能(取決於TaskScheduler)使用調用Wait的線程來執行Task,這種情況下調用Wait的線程不會阻塞,它會執行Task並立即返回。好處在於沒有線程會被阻塞,所以減少了資源占用。不好的地方在於加入線程在調用Wait前已經獲得了一個線程同步鎖,而Task試圖獲取同一個鎖,就會造成死鎖的線程。

2.任務執行方式(TaskCreationOptions)

我們知道為了創建一個Task,需要調用構造函數並傳遞一個ActionAction<object>委托,如果傳遞的是期待一個Object的方法,還必須向Task的構造函數穿都要傳給操作的實參。還可以選擇向構造器傳遞一些TaskCreationOptions標記來控制Task的執行方式。

 TaskCreationOptions為枚舉類型

枚舉值 說明
None 默認。
PreferFairness 盡可能公平的方式安排任務,即先進先執行。
LongRunning 指定任務將是長時間運行的,會新建線程執行,不會使用池化線程。
AttachedToParent 指定將任務附加到任務層次結構中的某個父級
DenyChildAttach 任務試圖和這個父任務連接將拋出一個InvalidOperationException
HideScheduler 強迫子任務使用默認調度而非父級任務調度

在默認情況下,Task內部是運行在池化線程上,這種線程會非常適合執行短計算密集作業。如果要執行長阻塞操作,則要避免使用池化線程。

在池化線程上運行一個長任務問題不大,但是如果要同時運行多個長任務(特別是會阻塞的任務),則會對性能產生影響。最好使用:TaskCreationOptions.LongRunning

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             int workerThreadsCount, completionPortThreadsCount;
 6             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
 7             Console.WriteLine("剩余工作線程數:{0},剩余IO線程數{1},主線程Id:{2}", workerThreadsCount, completionPortThreadsCount, Thread.CurrentThread.ManagedThreadId);
 8             Task task = Task.Factory.StartNew(() =>
 9             {
10                 Console.WriteLine("長任務執行,線程Id:{0}", Thread.CurrentThread.ManagedThreadId);
11                 Thread.Sleep(2000);
12             }, TaskCreationOptions.LongRunning);
13             Thread.Sleep(1000);
14             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
15             Console.WriteLine("剩余工作線程數:{0},剩余IO線程數{1},主線程Id:{2}", workerThreadsCount, completionPortThreadsCount, Thread.CurrentThread.ManagedThreadId);
16             Console.ReadKey();
17         }
18     }

執行結果如下:

注意

如果使運行I/O密集任務,則可以使用TaskCompletionSource和異步函數(asynchronous functions),通過回調(延續)實現並發性,而是不通過線程實現。

如果使運行計算密集性任務,則可以使用一個生產者/消費者隊列,控制這些任務的並發數量,避免出現線程和進程阻塞的問題。

 

延續(continuation)&延續選項(TaskContinuationOptions)

延續(continuation)會告訴任務在完成后繼續執行下面的操作。延續通常由一個回調方法實現,它會在操作完成之后執行一次。給一個任務附加延續的方法有兩種

1.GetAwaiter

任務的方法GetAwaiter是Framework 4.5新增加的,而C# 5.0的異步功能使用了這種方法,因此它非常重要。給一個任務附加延續如下:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Task<int> task = Task.Run(() =>
 6              {
 7                  int total = 0;
 8                  for (int i = 0; i <= 100; i++)
 9                  {
10                      total += i;
11                  }
12                  Thread.Sleep(2000);
13                  return total;
14              });
15             var awaiter = task.GetAwaiter();
16             awaiter.OnCompleted(() =>
17             {
18                 int result = awaiter.GetResult();//在延續中獲取Task的執行結果 19                 Console.WriteLine(result);
20             });
21             Console.ReadKey();
22         }
23     }

執行結果控制台會打印:5050。

調用GetAwaiter會返回一個等待者(awaiter)對象,它會讓先導(antecedent)任務在任務完成(或出錯)之后執行一個代理。已經完成的任務也可以附加一個延續,這事延續會馬上執行。

注意

1.等待者(awaiter)可以是任意對象,但必須包含特定的兩個方法和一個Boolean類型屬性。

1   public struct TaskAwaiter<TResult> : ICriticalNotifyCompletion, INotifyCompletion
2     {
3         public bool IsCompleted { get; }
4         public TResult GetResult();
5         public void OnCompleted(Action continuation);
6     }

2.先導任務出現錯誤,那么當延續代碼調用awaiter.GetResult()時就會重新拋出異常。我們可以需要調用GetResult,而是直接訪問先導任務的Result屬性(task.Result)。

GetResult的好處是,當先導任務出現錯誤時,異常可以直接拋出而不封裝在AggregateException中。

3.如果出現同步上下文,那么會自動捕捉它,然后延續提交到這個上下文中。在無需同步上下文的情況下通常不采用這種方法,使用ConfigureAwait代替它。它通常會使延續運行在先導任務所在的線程上,從而避免不必要的過載。

1    var awaiter = task.ConfigureAwait(false).GetAwaiter();

2.ContinueWith

另一種附加延續的方法是調用任務的ContinueWith方法:

 1         static void Main(string[] args)
 2         {
 3             Task<int> task = Task.Run(() =>
 4             {
 5                 int total = 0;
 6                 for (int i = 0; i <= 100; i++)
 7                 {
 8                     total += i;
 9                 }
10                 Thread.Sleep(2000);
11                 return total;
12             });
13             task.ContinueWith(continuationAction =>
14             {
15                 int result = continuationAction.Result;
16                 Console.WriteLine(result);
17             });
18             Console.ReadKey();
19         }

ContinueWith本身會返回一個Task,它非常適用於添加更多的延續。然后如果任務出現錯誤,我們必須直接處理AggregateException。

如果想讓延續運行在統一個線程上,必須指定 TaskContinuationOptions.ExecuteSynchronously;否則它會彈回線程池。ContinueWith特別適用於並行編程場景。

3.延續選項(TaskContinuationOptions)

在使用ContinueWith時可以指定任務的延續選項即TaskContinuationOptions,它的前六個枚舉類型與之前說的TaskCreationOptions枚舉提供的標志完全一樣,補充后續幾個枚舉值:

枚舉值 說明
LazyCancellation 除非先導任務完成,否則禁止延續任務完成(取消)。
NotOnRanToCompletion 指定不應在延續任務前面的任務已完成運行的情況下安排延續任務。
NotOnFaulted 指定不應在延續任務前面的任務引發了未處理異常的情況下安排延續任務。
NotOnCanceled 指定不應在延續任務前面的任務已取消的情況下安排延續任務。 
OnlyOnCanceled 指定只應在延續前面的任務已取消的情況下安排延續任務。
OnlyOnFaulted 指定只有在延續任務前面的任務引發了未處理異常的情況下才應安排延續任務。
OnlyOnRanToCompletion 指定只有在延續任務前面的任務引發了未處理異常的情況下才應安排延續任務。
ExecuteSynchronously 指定希望由先導任務的線程執行,先導任務完成后線程繼續執行延續任務。

 

ExecuteSynchronously是指同步執行,兩個任務都在同一個=線程一前一后的執行。

ContinueWith結合TaskContinuationOptions使用的示例:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Task<int> task = Task.Run(() =>
 6             {
 7                 int total = 0;
 8                 for (int i = 0; i <= 100; i++)
 9                 {
10                     total += i;
11                 }
12                 if (total == 5050)
13                 {
14                     throw new Exception("錯誤");//這段代碼可以注釋或開啟,用於測試
15                 }
16                 return total;
17             });
18             //指定先導任務無報錯的延續任務
19             task.ContinueWith(continuationAction =>
20             {
21                 int result = continuationAction.Result;
22                 Console.WriteLine(result);
23             }, TaskContinuationOptions.NotOnFaulted);
24             //指定先導任務報錯時的延續任務
25             task.ContinueWith(continuationAction =>
26             {
27                 foreach (Exception ex in continuationAction.Exception.InnerExceptions)//有關AggregateException異常處理后續討論 28                 {
29                     Console.WriteLine(ex.Message);
30                 }
31             }, TaskContinuationOptions.OnlyOnFaulted);
32             Console.ReadKey();
33         }
34     }

執行結果會打印:報錯,如果注釋掉拋出異常的代碼則會打印5050。

 

TaskCompletionSource

另一種創建任務的方法是使用TaskCompletionSource。它允許創建一個任務,並可以任務分發給使用者,並且這些使用者可以使用該任務的任何成員。它的實現原理是通過一個可以手動操作的“附屬”任務,用於指示操作完成或出錯的時間。

TaskCompletionSource的真正作用是創建一個不綁定線程的任務(手動控制任務工作流,可以使你把創建任務和完成任務分開)

這種方法非常適合I/O密集作業:可以利用所有任務的優點(它們能夠生成返回值、異常和延續),但不會在操作執行期間阻塞線程。

例如,假設一個任務需要等待2秒,然后返回10,我們的方法會返回在一個2秒后完成的任務,通過給任務附加一個延續就可以在不阻塞任何線程的前提下打印這個結果,如下:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             var awaiter = Demo(2000).GetAwaiter();//得到任務通過延續輸出返回值
 6             awaiter.OnCompleted(() =>
 7             {
 8                 Console.WriteLine(awaiter.GetResult());
 9             });
10             Console.WriteLine("主線程繼續執行....");
11             Console.ReadKey();
12         }
13         static Task<int> Demo(int millis)
14         {
15             //創建一個任務完成源
16             TaskCompletionSource<int> taskCompletionSource = new TaskCompletionSource<int>();
17             var timer = new System.Timers.Timer(millis) { AutoReset = false };
18             timer.Elapsed += delegate
19             {
20                 timer.Dispose(); taskCompletionSource.SetResult(10);//寫入返回值
21             };
22             timer.Start();
23             return taskCompletionSource.Task;//返回任務
24         }
25     }

執行結果:

注意:如果多次調用SetResultSetExceptionSetCanceled,它們會拋出異常,而TryXXX會返回false。

 

任務取消(CancellationTokenSource)

一些情況下,后台任務可能運行很長時間,取消任務就非常有用了。.NET提供了一種標准的任務取消機制可用於基於任務的異步模式

取消基於CancellationTokenSource類,該類可用於發送取消請求。請求發送給引用CancellationToken類的任務,其中CancellationToken類與CancellationTokenSource類相關聯。

使用示例如下:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //構造函數 指定延遲2秒后自動取消任務
 6             CancellationTokenSource source = new CancellationTokenSource(2000);
 7             //注冊一個任務取消后執行的委托
 8             source.Token.Register(() =>
 9             {
10                 Console.WriteLine("線程Id:{0} 任務被取消后的業務邏輯正在運行", Thread.CurrentThread.ManagedThreadId);
11             });
12             //啟動任務,將取消標記源帶入參數
13             Task.Run(() =>
14             {
15                 while (!source.IsCancellationRequested)//IsCancellationRequested為True時取消任務 16                 {
17                     Thread.Sleep(100);
18                     Console.WriteLine("線程Id:{0} 任務正在運行", Thread.CurrentThread.ManagedThreadId);
19                 }
20             }, source.Token);
21             //主線程掛起2秒后手動取消任務
22             {
23                 //Thread.Sleep(2000);
24                 //source.Cancel();//手動取消任務
25             }
26             //主線程不阻塞,2秒后自動取消任務
27             {
28                 source.CancelAfter(2000);
29             }
30             Console.ReadKey();
31         }
32     }

執行結果:

根據Register方法綁定任務取消后的委托

1   public CancellationTokenRegistration Register(Action callback);
2   public CancellationTokenRegistration Register(Action callback, bool useSynchronizationContext);
3   public CancellationTokenRegistration Register(Action<object> callback, object state);
4   public CancellationTokenRegistration Register(Action<object> callback, object state, bool useSynchronizationContext);

手動取消任務Cancel方法

自動取消任務

1.CancelAfter方法后面可以帶入參數指定延遲多少后時間取消任務。

1   public void CancelAfter(TimeSpan delay);
2   public void CancelAfter(int millisecondsDelay);

2.CancellationTokenSource構造函數可以帶入參數指定延遲多少時間后取消任務。

1   public CancellationTokenSource(TimeSpan delay);
2   public CancellationTokenSource(int millisecondsDelay);

任務綁定CancellationTokenSource對象,在Task源碼中可以帶入CancellationToken對象的啟動任務方式都可以綁定CancellationTokenSource

 

異步等待 (Task.Delay)

 異步等待非常實用,因此它成為Task類的一個靜態方法

 常用的使用方式有2種,如下:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //第1種
 6             {
 7                 Task.Delay(2000).ContinueWith((o) =>
 8                 {
 9                     Console.WriteLine("線程Id:{0},異步等待2秒后執行的邏輯", Thread.CurrentThread.ManagedThreadId);
10                 });
11             }
12             //第2種
13             {
14                 Task.Delay(3000).GetAwaiter().OnCompleted(() =>
15                 {
16                     Console.WriteLine("線程Id:{0},異步等待3秒后執行的邏輯", Thread.CurrentThread.ManagedThreadId);
17                 });
18             }
19             Console.WriteLine("主線程Id:{0},繼續執行", Thread.CurrentThread.ManagedThreadId);
20             Console.ReadKey();
21         }
22     }

執行結果如下:

Task.DelayThread.Sleep的異步版本。而它們的區別如下(引自 禪道 ):

1.Thread.Sleep 是同步延遲,Task.Delay異步延遲。

2.Thread.Sleep 會阻塞線程,Task.Delay不會。

3.Thread.Sleep不能取消,Task.Delay可以。

4. Task.Delay() 比 Thread.Sleep() 消耗更多的資源,但是Task.Delay()可用於為方法返回Task類型;或者根據CancellationToken取消標記動態取消等待。

5. Task.Delay() 實質創建一個運行給定時間的任務, Thread.Sleep() 使當前線程休眠給定時間。

 

異常(AggregateException)

與線程不同,任務可以隨時拋出異常。所以,如果任務中的代碼拋出一個未處理異常,那么這個異常會自動傳遞到調用Wait()Task<TResult>Result屬性的代碼上。
任務的異常將會自動捕獲並拋給調用者。為確保報告所有的異常,CLR會將異常封裝在AggregateException容器中,該容器公開的InnerExceptions屬性中包含所有捕獲的異常,從而更適合並行編程。

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             try
 6             {
 7                 Task.Run(() =>
 8                 {
 9                     throw new Exception("錯誤");
10                 }).Wait();
11             }
12             catch (AggregateException axe)
13             {
14                 foreach (var item in axe.InnerExceptions)
15                 {
16                     Console.WriteLine(item.Message);
17                 }
18             }
19             Console.ReadKey();
20         }
21     }

上述示例控制台會顯示:錯誤

注意

使用TaskIsFaultedIsCanceled屬性,就可以不重新拋出異常而檢測出錯的任務。
1.IsFaultedIsCanceled都返回False,表示沒有錯誤發生。
2.IsCanceledTrue,則任務拋出了OperationCanceledOperation(取消線程正在執行的操作時在線程中拋出的異常)。
3.IsFaultedTrue,則任務拋出另一種異常,而Exception屬性包含了該錯誤。

1.Flatten

當子任務拋出異常時,通過調用Flatten方法,可以消除任意層次的嵌套以簡化異常處理。

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             var parent = Task.Factory.StartNew(() =>
 6             {
 7                 int[] numbers = { 0 };
 8                 var childFactory = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.None);
 9                 childFactory.StartNew(() => 10 / numbers[0]);//除零
10                 childFactory.StartNew(() => numbers[1]);//超出索引范圍
11                 childFactory.StartNew(() => throw null);//空引用
12             });
13             try
14             {
15                 parent.Wait();
16             }
17             catch (AggregateException axe)
18             {
19                 foreach (var item in axe.Flatten().InnerExceptions)
20                 {
21                     Console.WriteLine(item.Message);
22                 }
23             }
24             Console.ReadKey();
25         }
26     }

2.Handle

 如果需要只捕獲特定類型異常,並重拋其它類型的異常,Handle方法為此提供了一種快捷方式。

Handle接受一個predicate(異常斷言),並在每個內部異常上運行此斷言。

1 public void Handle(Func<Exception, bool> predicate);

如果斷言返回True,它認為該異常是“已處理”,當所有異常過濾之后:

1.如果所有異常是已處理的,異常不會拋出。

2.如果存在異常未處理,就會構造一個新的AggregateException對象來包含這些異常並拋出。

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             var parent = Task.Factory.StartNew(() =>
 6             {
 7                 int[] numbers = { 0 };
 8                 var childFactory = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.None);
 9                 childFactory.StartNew(() => 10 / numbers[0]);//除零
10                 childFactory.StartNew(() => numbers[1]);//超出索引范圍
11                 childFactory.StartNew(() => throw null);//空引用
12             });
13             try
14             {
15                 try
16                 {
17                     parent.Wait();
18                 }
19                 catch (AggregateException axe)
20                 {
21                     axe.Flatten().Handle(ex =>
22                     {
23                         if (ex is DivideByZeroException)
24                         {
25                             Console.WriteLine("除零-錯誤處理完畢");
26                             return true;
27                         }
28                         if (ex is IndexOutOfRangeException)
29                         {
30                             Console.WriteLine("超出索引范圍-錯誤處理完畢");
31                             return true;
32                         }
33                         return false;//所有其它 異常重新拋出
34                     });
35 
36                 }
37             }
38             catch (AggregateException axe)
39             {
40                 foreach (var item in axe.InnerExceptions)//捕獲重新拋出的異常
41                 {
42                     Console.WriteLine(item.Message);
43                 }
44             }
45             Console.ReadKey();
46         }
47     }

執行結果:

 

 結語

1.async和await這兩個關鍵字下篇記錄。

2.任務調度器(TaskScheduler)是Task之所以如此靈活的本質,我們常說Task是在ThreadPool上更升級化的封裝,其實很大程度上歸功於這個對象,考慮下篇要不要說一下,但其實我看的都頭疼...

3.Task類包含很多的重載,最好F12跳到Task內熟悉下結構。

 

參考文獻 

CLR via C#(第4版) Jeffrey Richter

C#高級編程(第10版) C# 6 & .NET Core 1.0   Christian Nagel  

果殼中的C# C#5.0權威指南  Joseph Albahari

C#並發編程 經典實例  Stephen Cleary

...

 


免責聲明!

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



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