《CLR Via C# 第3版》笔记之(十九) - 任务(Task)
除了上篇中提到的線程池,本篇介紹一種新的實現異步操作的方法--任務(Task)。
主要內容:
- 任務的介紹
- 任務的基本應用
- 子任務和任務工廠
- 任務調度器
- 并行任務Parallel
?
1. 任務的介紹
利用ThreadPool的QueueUserWorkItem方法建立的異步操作存在一些限制:
?
而使用任務(Task)來建立異步操作可以克服上述限制,同時還解決了其他一些問題。
任務(Task)對象和線程池相比,多了很多狀態字段和方法,便于更好的控制任務(Task)的運行。
當然,任務(Task)提供大量的功能也是有代價的,意味著更多的內存消耗。所以在實際使用中,如果不用任務(Task)的附加功能,那么就使用ThreadPool的QueueUserWorkItem方法。
?
通過任務的狀態(TaskStatus),可以了解任務(Task)的生命周期。
TaskStatus是一個枚舉類型,定義如下:
public enum TaskStatus { // 運行前狀態Created = 0, // 任務被顯式創建,通過Start()開始這個任務WaitingForActivation = 1, // 任務被隱式創建,會自動開始WaitingToRun = 2, // 任務已經被調度,但是還沒有運行// 運行中狀態Running = 3, // 任務正在運行WaitingForChildrenToComplete = 4, // 等待子任務完成// 運行完成后狀態RanToCompletion = 5, // 任務正常完成Canceled = 6, // 任務被取消Faulted = 7, // 任務出錯 }構造一個Task后,它的狀態為Create。
啟動后,狀態變為WaitingToRun。
實際在一個線程上運行時,狀態變為Running。
運行完成后,根據實際情況,狀態變為RanToCompletiion,Canceled,Faulted三種中的一種。
如果Task不是通過new來創建的,而是通過以下某個函數創建的,那么它的狀態就是WaitingForActivation:
ContinueWith,ContinueWhenAll,ContinueWhenAny,FromAsync。
如果Task是通過構造一個TaskCompletionSource<TResult>對象來創建的,該Task在創建時也是處于WaitingForActivation狀態。
?
2. 任務的基本應用
下面演示任務的創建,取消,等待等基本使用方法。
2.1 創建并啟動一個Task
using System; using System.Threading.Tasks; using System.Threading;public class CLRviaCSharp_19 {static void Main(string[] args){Console.WriteLine("Main Thread start!");// 創建一個TaskTask t1 = new Task(() => { Console.WriteLine("Task start"); Thread.Sleep(1000);Console.WriteLine("Task end");});// 啟動Taskt1.Start();// 主線程并沒有等待Task,在Task完成前就已經完成了Console.WriteLine("Main Thread end!");Console.ReadKey(true);} }?
2.2 主線程等待子線程完成
using System; using System.Threading.Tasks; using System.Threading;public class CLRviaCSharp_19 {static void Main(string[] args){Console.WriteLine("Main Thread start!");// 創建2個TaskTask t1 = new Task(() => { Console.WriteLine("Task1 start"); Thread.Sleep(1000);Console.WriteLine("Task1 end");});Task t2 = new Task(() =>{Console.WriteLine("Task2 start");Thread.Sleep(2000);Console.WriteLine("Task2 end");});// 啟動Taskt1.Start();t2.Start();// 當t1和t2中任何一個完成后,主線程繼續后面的操作// Task.WaitAny(new Task[] { t1, t2 });// 當t1和t2中全部完成后,主線程繼續后面的操作Task.WaitAll(new Task[] { t1, t2 });Console.WriteLine("Main Thread end!");Console.ReadKey(true);} }等待的方法WaitAll和WaitAny可根據應用場景選用一個。
?
2.3 取消Task
取消Task和取消一個線程類似,使用CancellationTokenSource。
using System; using System.Threading.Tasks; using System.Threading;public class CLRviaCSharp_19 {static void Main(string[] args){Console.WriteLine("Main Thread start!");CancellationTokenSource cts = new CancellationTokenSource();// 創建2個TaskTask t1 = new Task(() => { Console.WriteLine("Task1 start");for (int i = 0; i < 100; i++){if (!cts.Token.IsCancellationRequested){Console.WriteLine("Count : " + i.ToString());Thread.Sleep(1000);}else{Console.WriteLine("Task1 is Cancelled!");break;}}Console.WriteLine("Task1 end");}, cts.Token);// 啟動Taskt1.Start();Thread.Sleep(3000);// 運行3秒后取消Taskcts.Cancel();// 為了測試取消操作,主線程等待Task完成Task.WaitAny(new Task[] { t1 });Console.WriteLine("Main Thread end!");Console.ReadKey(true);} }?
3. 子任務和任務工廠
3.1 延續任務
為了保證程序的伸縮性,應該盡量避免線程阻塞,這就意味著我們在等待一個任務完成時,最好不要用Wait,而是讓一個任務結束后自動啟動它的下一個任務。
using System; using System.Threading.Tasks; using System.Threading;public class CLRviaCSharp_19 {static void Main(string[] args){Console.WriteLine("Main Thread start!");// 第一個TaskTask<int> t1 = new Task<int>(() =>{Console.WriteLine("Task 1 start!");Thread.Sleep(2000);Console.WriteLine("Task 1 end!");return 1;});// 啟動第一個Taskt1.Start();// 因為TaskContinuationOptions.OnlyOnRanToCompletion,// 所以第一個Task正常結束時,啟動第二個Task。// TaskContinuationOptions.OnlyOnFaulted,則第一個Task出現異常時,啟動第二個Task// 其他可詳細參考TaskContinuationOptions定義的各個標志t1.ContinueWith(AnotherTask, TaskContinuationOptions.OnlyOnRanToCompletion);Console.WriteLine("Main Thread end!");Console.ReadKey(true);}// 第二個Task的處理都在AnotherTask函數中,// 第二個Task的引用其實就是上面ContinueWith函數的返回值。// 這里沒有保存第二個Task的引用private static void AnotherTask(Task<int> task){Console.WriteLine("Task 2 start!");Thread.Sleep(1000);Console.WriteLine("Task 1's return Value is : " + task.Result);Console.WriteLine("Task 2 end!");} }?
3.2 子任務
定義子任務時,注意一定要加上TaskCreationOptions.AttachedToParent,這樣父任務會等待子任務執行完后才結束。
using System; using System.Threading.Tasks; using System.Threading;public class CLRviaCSharp_19 {static void Main(string[] args){Console.WriteLine("Main Thread start!");Task<int[]> parentTask = new Task<int[]>(() =>{var result = new int[3];// 子任務1new Task(() => { Console.WriteLine("sub task 1 start!"); Thread.Sleep(1000);Console.WriteLine("sub task 1 end!");result[0] = 1;}, TaskCreationOptions.AttachedToParent).Start();// 子任務2new Task(() =>{Console.WriteLine("sub task 2 start!");Thread.Sleep(1000);Console.WriteLine("sub task 2 end!");result[1] = 2;}, TaskCreationOptions.AttachedToParent).Start();// 子任務3new Task(() =>{Console.WriteLine("sub task 3 start!");Thread.Sleep(1000);Console.WriteLine("sub task 3 end!");result[2] = 3;}, TaskCreationOptions.AttachedToParent).Start();return result;});parentTask.Start();Console.WriteLine("Parent Task's Result is :");foreach (int result in parentTask.Result)Console.Write("{0}\t", result);Console.WriteLine();Console.WriteLine("Main Thread end!");Console.ReadKey(true);} }上面的例子中,可以把TaskCreationOptions.AttachedToParent刪掉試試,打印出來的Result應該是3個0,而不是1? 2?? 3。
3個子任務的執行順序也和定義的順序無關,比如任務3可能最先執行(與CPU的調度有關)。
?
3.3 任務工廠
除了上面的方法,還可以使用任務工廠來批量創建任務。
using System; using System.Threading.Tasks; using System.Threading;public class CLRviaCSharp_19 {static void Main(string[] args){Console.WriteLine("Main Thread start!");Task<int[]> parentTask = new Task<int[]>(() =>{var result = new int[3];TaskFactory tf = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.None);// 子任務1tf.StartNew(() =>{Console.WriteLine("sub task 1 start!");Thread.Sleep(1000);Console.WriteLine("sub task 1 end!");result[0] = 1;});// 子任務2tf.StartNew(() =>{Console.WriteLine("sub task 2 start!");Thread.Sleep(1000);Console.WriteLine("sub task 2 end!");result[1] = 2;});// 子任務3tf.StartNew(() =>{Console.WriteLine("sub task 3 start!");Thread.Sleep(1000);Console.WriteLine("sub task 3 end!");result[2] = 3;});return result;});parentTask.Start();Console.WriteLine("Parent Task's Result is :");foreach (int result in parentTask.Result)Console.Write("{0}\t", result);Console.WriteLine();Console.WriteLine("Main Thread end!");Console.ReadKey(true);} }使用任務工廠與上面3.2中直接定義子任務相比,優勢主要在于可以共享子任務的設置,比如在TaskFactory中設置了TaskCreationOptions.AttachedToParent,那么它啟動的子任務都具有這個屬性了。
當然,任務工廠(TaskFactory)還提供了很多控制子任務的函數,用的時候可以看看它的類定義。
?
4. 任務調度器
上面例子中任務的各種操作(運行,等待,取消等等),都是由CLR的任務調度器來調度的。
?
FCL公開了2種任務調度器:線程池任務調度器和同步上下文任務調度器。
默認情況下,應用程序都是使用的線程池任務調度器。WPF和Winform中通常使用同步上下文任務調度器。
?
CLR的任務調度器類(TaskScheduler)中有個Default屬性返回的就是線程池任務調度器。
還有個FromCurrentSynchronizationContext方法,返回的是同步上下文任務調度器。
?
我們也可以通過繼承CLR中的任務調度器(TaskScheduler)來定制適合自己業務需要的任務調度器。
下面我們定制一個簡單的TaskScheduler,將3.3中每個子任務的打印信息的功能移到自定義的任務調度器MyTaskScheduler中。
using System; using System.Threading.Tasks; using System.Threading; using System.Collections.Generic;public class CLRviaCSharp_19 {static void Main(string[] args){Console.WriteLine("Main Thread start!");Task<int[]> parentTask = new Task<int[]>(() =>{var result = new int[3];// 這里的TaskFactory中指定的是自定義的任務調度器MyTaskSchedulerTaskFactory tf = new TaskFactory(CancellationToken.None, TaskCreationOptions.AttachedToParent,TaskContinuationOptions.None, new MyTaskScheduler());// 子任務1tf.StartNew(() =>{Thread.Sleep(1000);result[0] = 1;});// 子任務2tf.StartNew(() =>{Thread.Sleep(1000);result[1] = 2;});// 子任務3tf.StartNew(() =>{Thread.Sleep(1000);result[2] = 3;});return result;});parentTask.Start();Console.WriteLine("Parent Task's Result is :");foreach (int result in parentTask.Result)Console.Write("{0}\t", result);Console.WriteLine();Console.WriteLine("Main Thread end!");Console.ReadKey(true);} }// 自定義的TaskScheduler,沒什么實際的作用,只是為了實驗自定義TaskScheduler public class MyTaskScheduler : TaskScheduler {private IList<Task> _lstTasks;public MyTaskScheduler(){_lstTasks = new List<Task>();}#region inherit from TaskSchedulerprotected override System.Collections.Generic.IEnumerable<Task> GetScheduledTasks(){return _lstTasks;}protected override void QueueTask(Task task){_lstTasks.Add(task);// 將原先的打印信息,移到此處統一處理Console.WriteLine("task " + task.Id + " is start!");TryExecuteTask(task);Console.WriteLine("task " + task.Id + " is end!");}protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued){return TryExecuteTask(task);}#endregion }?
5. 并行任務Parallel
Parallel是為了簡化任務編程而新增的靜態類,利用Parallel可以將平時的循環操作都并行起來。
下例演示了for并行循環,foreach并行循環與之類似。
using System; using System.Threading.Tasks; using System.Threading; using System.Diagnostics;public class CLRviaCSharp_19 {static void Main(string[] args){Console.WriteLine("Main Thread start!");int max = 10;// 普通循環long start = Stopwatch.GetTimestamp();for (int i = 0; i < max; i++){Thread.Sleep(1000);}Console.WriteLine("{0:N0}", Stopwatch.GetTimestamp() - start);// 并行的循環start = Stopwatch.GetTimestamp();Parallel.For(0, max, i => { Thread.Sleep(1000); });Console.WriteLine("{0:N0}", Stopwatch.GetTimestamp() - start);Console.WriteLine("Main Thread end!");Console.ReadKey(true);} }在上面的例子中,采用并行循環消耗的時間不到原先的一半。
但是,采用并行循環需要滿足一個條件,就是for循環中的內容能夠并行才行。
比如for循環中是個對 循環變量i 進行的累加操作(例如sum += i;),那就不能使用并行循環。
?
還有一點需要注意,Parallel的方法本身有開銷。
所以如果for循環內的處理比較簡單的話,那么直接用for循環可能更快一些。
比如將上例中的Thread.Sleep(1000);刪掉,再運行程序發現,直接for循環要快很多。
轉載于:https://www.cnblogs.com/wang_yb/archive/2011/11/10/2244745.html
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的《CLR Via C# 第3版》笔记之(十九) - 任务(Task)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [unix shell笔记] - 和fi
- 下一篇: WOLF ISP CCIE 方向优惠最后