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