一、簡介
在4.0之前,多線程只能用Thread或者ThreadPool,而4.0下提供了功能強大的Task處理方式,這樣免去了程序員自己維護線程池,而且可以申請取消線程等。。。所以本文主要描述Task的特性。
二、Task的優點
操作系統自身可以實現線程,並且提供了非托管的API來創建與管理這些線程。但是C#是運行在CLR上面的,為了方便的創建與管理線程,CLR對這些API進行了封裝,通過System.Threading.Tasks.Task公開了這些包裝。
在計算機中,創建線程十分耗費珍貴的計算機資源,所以Task啟動時,不是直接創建一個線程。而是從線程池請求一個線程。並且通過對線程的抽象,程序員一般和Task打交道就好,這樣降低了高效管理多線程的復雜度。
三、Task使用示例。
Task可以獲取一個返回值,下面的程序實現如下功能:利用Task啟動一個新的線程,然后計算3*5的值,並返回。

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 using System.Text; 6 7 namespace TaskTest 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 // 定義並啟動一個線程,計算5乘以3,並返回一個int類型的值 14 Task<int> task = Task.Factory.StartNew<int>( 15 () => { return 5 * 3; }); 16 // 線程啟動並開始執行 17 18 foreach (char busySymbol in Utility.BusySymbols()) 19 { 20 if (task.IsCompleted) 21 { 22 Console.Write('\b'); 23 break; 24 } 25 Console.Write(busySymbol); 26 } 27 Console.WriteLine(); 28 29 Console.WriteLine(task.Result.ToString()); 30 // 如果執行至此仍未完成那個線程,則輸出堆棧信息 31 System.Diagnostics.Trace.Assert(task.IsCompleted); 32 } 33 } 34 public class Utility 35 { 36 public static IEnumerable<char> BusySymbols() 37 { 38 string busySymbols = @"-\|/-\|/"; 39 int next = 0; 40 { 41 while (true) 42 { 43 yield return busySymbols[next]; 44 next = (++next) % busySymbols.Length; 45 yield return '\b'; 46 } 47 } 48 } 49 } 50 }
輸出結果如下兩種:
可以看出, 第一次運行如左圖。首次運行到if (task.IsCompleted)的時候,計算3*5個線程還沒有執行完,所以直接執行:
Console.Write(busySymbol);
輸出了“-”,第二次到if的時候,3*5計算完成,執行if里面的內容,輸出換行,跳出。然后執行到 Console.WriteLine(task.Result.ToString()); 輸出15
第二次運行如右圖。首次運行到if的時候,3*5已經計算完成,所以只輸出了一個空的換行。然后輸出15。其中接收返回值的語句是:
Console.WriteLine(task.Result.ToString());
當然,Task還有一套start的方法,但是不常用,用Task的靜態Factory屬性的StartNes方法就可以實例化並啟動一個線程了,而且,附帶指定了返回值的類型。
四、ContinueWith
Task包含了一個Continue的方法,這個方法可以將多個任務連接起來,可以指定當前線程完成之后啟動哪個或者哪些線程。ContinueWith會返回另外一個Task,所以工作鏈可以持續下去。
用法如下:

1 static void Main(string[] args) 2 { 3 Task<int> task = Task.Factory.StartNew<int>( 4 () => { return 3 * 5; }); 5 Task faultTask = task.ContinueWith( 6 (antecedentTask) => { 7 System.Diagnostics.Trace.Assert(task.IsFaulted); 8 Console.WriteLine("Task State:Faulted"); 9 },TaskContinuationOptions.OnlyOnFaulted); 10 Task canceledTask = task.ContinueWith( 11 (antecedentTask) => 12 { 13 System.Diagnostics.Trace.Assert(task.IsCanceled); 14 Console.WriteLine("Task State:Canceled"); 15 },TaskContinuationOptions.OnlyOnCanceled); 16 Task completedTask = task.ContinueWith( 17 (antecedentTask) => 18 { 19 System.Diagnostics.Trace.Assert(task.IsCompleted); 20 Console.WriteLine("Task State:Complete,Value is "+antecedentTask.Result.ToString()); 21 }, TaskContinuationOptions.OnlyOnRanToCompletion); 22 completedTask.Wait(); 23 }
ContinueWith的參數是一個與task(即后面任務的先驅任務的祖先)相同類型的Task參數。當啟動后代任務時,自動將先驅任務賦值給ContinueWith的參數,所以本例輸出結果是:
Task State:Complete,Value is 15.
如果我不使用completedTask.Wait();這一句,那么主線程完成后,不會去管task及其后續任務是否完成,就退出,所以加上了這 幾句話,這樣避免task與后繼任務執行完之前退出。這樣就可以將任務連接回調用線程(main)了。當然要注意,這個例子中只有 completeTask是存在的,因為task是正常執行的。不能用canceledTask.Wait(); 因為這個任務在task正常的情況下,永遠不會被執行。
五、異常處理
當然也是用try-catch捕捉異常,但是在哪何時捕捉異常,都是一個問題。
從CLR2.0開始,在終結器線程、線程池線程和用戶自己創建的線程中發生的未處理的異常一般會在異常層次結構中冒泡。如果冒泡到上一層,可以捕捉到這個異常,則十分好。Task支持這樣一個機制;
即,Task在執行期間發生了未處理的異常,這個異常會被禁止(suppressed),抑制,線程后面不繼續執行,標記為運行完成。直到調用某個 任務完成成員例如:Wait(),Result,Task.WaitAll()或者Task.WaitAny(),才會重新引發線程執行期間未處理的異 常,下面的代碼展示了這樣的機制:

1 static void Main(string[] args) 2 { 3 Task task = Task.Factory.StartNew( () => 4 { 5 throw new ApplicationException(); 6 Console.WriteLine("我之前有異常"); 7 }); // 顯式拋出一個異常 8 try 9 { 10 task.Wait(); 11 } 12 catch (AggregateException ex) 13 { 14 foreach (Exception e in ex.InnerExceptions) 15 { 16 Console.WriteLine(e.Message); 17 } 18 } 19 }
從task線程里面拋出了異常,從wait()的時候捕捉到了異常。注意 catch (AggregateException ex)里面的參數是 AggregateException,這是一個異常集合。
當然,還有另外一種方法來處理這種異常。就是使用前文提到的ContinueWith認為,利用ContinueWith()中的task參數,可以評估先驅任務的Exception屬性。代碼如下:

注意,並不是調用task.Wait()引發的異常,而是用faultedTask去檢查task是否產生了異常。
六、取消任務
Task中取代了粗暴的kill和absort,而是設置了一個變量,然后主線程可以申請取消子線程,當線程收到取消信號時,會執行完當前的一次,然后取消。
代碼如下:

1 static void Main(string[] args) 2 { 3 string start = "*".PadRight(Console.WindowWidth - 1, '*'); 4 Console.WriteLine("Push ENTER to exit."); 5 6 CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); 7 8 // 附加了一個token參數,是否取消的標志 9 Task task = Task.Factory.StartNew( 10 () => WriteChar(cancellationTokenSource.Token), cancellationTokenSource.Token); 11 // 等待輸入任何一個字符 12 Console.ReadLine(); 13 // 請求取消 14 cancellationTokenSource.Cancel(); 15 Console.WriteLine(start); 16 task.Wait(); 17 Console.ReadLine(); 18 } 19 private static void WriteChar(CancellationToken cancellationToken) 20 { 21 int i = 0; 22 string charChain = string.Empty; 23 // 無取消請求的時候 24 while (!cancellationToken.IsCancellationRequested || i == int.MaxValue) 25 { 26 charChain += "tom"+i.ToString() + "\n"; 27 Console.WriteLine(charChain); 28 } 29 }
上述代碼中,cancellationTokenSource.Cancel()與 task.wait();之間打印星號,我們在運行結果中可能會發現,在星號后面仍然輸出了一個char,因為cancel后,線程不會馬上終止,而是執 行完當前的代碼,然后直到下次判斷是否終止后才終止。
不過這個例子中,WirteChar中的while循環太短暫,所以沒有很好的展示出task可能的額外的一次執行。
七、多線程編程中,三種方法都可選的情況下,優先使用Task的方式,其次使用Threadpool,最次之使用Thread