浅谈.Net异步编程的前世今生----TPL篇
前言
我們?cè)诖饲耙呀?jīng)介紹了APM模型和EAP模型,以及它們的優(yōu)缺點(diǎn)。在EAP模型中,可以實(shí)時(shí)得知異步操作的進(jìn)度,以及支持取消操作。但是組合多個(gè)異步操作仍需大量工作,編寫大量代碼方可完成。
因此,在.Net Framework 4.0中,引入了一個(gè)新的關(guān)于異步操作的模型,叫做任務(wù)并行庫,簡稱為TPL。
第三個(gè)異步編程模型:TPL
概述
TPL,全稱為Task Parallel Library,它可以被認(rèn)為是線程池之上的又一個(gè)抽象層,隱藏了部分底層細(xì)節(jié),核心概念為任務(wù)。
一個(gè)任務(wù)代表了一個(gè)異步操作,該操作可以通過多種方式運(yùn)行,可以使用或者不使用獨(dú)立線程(如Thread)運(yùn)行,還可以通過多種方式和其他任務(wù)組合起來。
在本文中,我們將探究TPL的使用方式,以及如何正確處理異常,取消任務(wù),如何使多個(gè)任務(wù)同時(shí)執(zhí)行等。
創(chuàng)建TPL
我們首先需要?jiǎng)?chuàng)建一個(gè)控制臺(tái)程序,用來執(zhí)行Task的創(chuàng)建和運(yùn)行,并在Task內(nèi)部使用委托調(diào)用一個(gè)方法,用來打印當(dāng)前任務(wù)以及當(dāng)前任務(wù)所在的線程信息,如圖所示:
我們分別使用了三種方式來創(chuàng)建任務(wù)并執(zhí)行:
在第一種方式中,使用new Task類的方式,把需要執(zhí)行的內(nèi)容放入Action委托并傳入?yún)?shù),最后使用Start方法開啟任務(wù)執(zhí)行,若不調(diào)用Start方法,則不會(huì)啟動(dòng)任務(wù),切記。
在第二種方式和第三種方式中,被創(chuàng)建的任務(wù)會(huì)立即開始工作,所以無需顯式調(diào)用Start方法。Task.Run與Task.Factory.StartNew的區(qū)別為,前者是后者的一個(gè)快捷方式,但后者擁有附加選項(xiàng),如沒有特殊需求,通常使用前者來創(chuàng)建任務(wù)。
相關(guān)代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks;namespace TPLDemo {class Program{static void Main(string[] args){var t1 = new Task(() => TaskMethod("任務(wù)1"));var t2 = new Task(() => TaskMethod("任務(wù)2"));t1.Start();t2.Start();Task.Run(() => TaskMethod("任務(wù)3"));Task.Factory.StartNew(() => TaskMethod("任務(wù)4"));Task.Factory.StartNew(() => TaskMethod("任務(wù)5"), TaskCreationOptions.LongRunning);Thread.Sleep(TimeSpan.FromSeconds(1000));}/// <summary>/// 任務(wù)運(yùn)行的方法/// </summary>/// <param name="name">The name.</param>static void TaskMethod(string name){Console.WriteLine($@"Task {name} 是一個(gè)正在線程id為 {Thread.CurrentThread.ManagedThreadId} 上運(yùn)行的任務(wù),是否為線程池線程:{Thread.CurrentThread.IsThreadPoolThread}");}} }接著我們來看一下運(yùn)行結(jié)果,如圖所示:
可以看出任務(wù)1,2,3,4均為線程池中的線程,也印證了我們此前的概念,TPL為線程池上的一個(gè)抽象層。而任務(wù)5在實(shí)現(xiàn)時(shí)被我們標(biāo)記為需要長時(shí)間運(yùn)行的任務(wù),因此在調(diào)度時(shí),并未使用線程池中的線程,而是單獨(dú)開啟一個(gè)線程執(zhí)行,這樣可以避免線程池中的線程被長時(shí)間占用,無法復(fù)用資源。
實(shí)現(xiàn)取消
在EAP模型中,我們借助BackgroundWorker組件封裝好的取消方法,可以對(duì)正在執(zhí)行的線程進(jìn)行取消。那么這樣的方式畢竟是有很大的局限性的,因此,在Net Framework 4.0中,微軟創(chuàng)建了統(tǒng)一的模型來協(xié)作取消涉及兩個(gè)對(duì)象的異步操作或長時(shí)間運(yùn)行的同步操作,它就是CancellationTokenSource和CancellationToken。
我們需要?jiǎng)?chuàng)建CancellationTokenSource實(shí)例以傳入Task,來標(biāo)識(shí)此任務(wù)包含外部取消操作,然后使用CancellationToken來傳播任務(wù)內(nèi)的應(yīng)取消操作的通知,如圖所示:
相關(guān)代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks;namespace TPLDemo {class Program{static void Main(string[] args){var cts = new CancellationTokenSource();var longTask = new Task<int>(() => TaskMethod("任務(wù)1", 10, cts.Token), cts.Token);Console.WriteLine(longTask.Status);cts.Cancel();Console.WriteLine(longTask.Status);Console.WriteLine("任務(wù)1在執(zhí)行前已經(jīng)被取消");cts = new CancellationTokenSource();longTask = new Task<int>(() => TaskMethod("任務(wù)2", 10, cts.Token), cts.Token);longTask.Start();for (int i = 0; i < 5; i++){Thread.Sleep(TimeSpan.FromSeconds(0.5));Console.WriteLine(longTask.Status);}cts.Cancel();for (int i = 0; i < 5; i++){Thread.Sleep(TimeSpan.FromSeconds(0.5));Console.WriteLine(longTask.Status);}Console.WriteLine($"任務(wù)2執(zhí)行完成,結(jié)果:{longTask.Result}");Console.Read();}/// <summary>/// 任務(wù)取消的方法/// </summary>/// <param name="name"></param>/// <param name="seconds"></param>/// <param name="token"></param>/// <returns></returns>private static int TaskMethod(string name, int seconds, CancellationToken token){Console.WriteLine($@"Task {name} 是一個(gè)正在線程id為 {Thread.CurrentThread.ManagedThreadId} 上運(yùn)行的任務(wù),是否為線程池線程:{Thread.CurrentThread.IsThreadPoolThread}");for (int i = 0; i < seconds; i++){Thread.Sleep(TimeSpan.FromSeconds(1));if (token.IsCancellationRequested){return -1;}}return 42 * seconds;}} }運(yùn)行后結(jié)果如圖所示:
從代碼中,我們可以看出,我們給Task傳遞了兩次CancellationTokenSource,一次是任務(wù)內(nèi)執(zhí)行方法,一次是任務(wù)本身構(gòu)造函數(shù),那么為什么要這樣做呢?
因?yàn)槿绻覀冊(cè)谌蝿?wù)啟動(dòng)之前進(jìn)行取消,那么該任務(wù)所在的TPL模型,就會(huì)“接管”該取消操作,因?yàn)檫@些代碼根本不會(huì)繼續(xù)執(zhí)行。我們查看第一個(gè)任務(wù)的狀態(tài)可以得知,它已經(jīng)被取消了,如果在此時(shí)再調(diào)用Start方法,那么將會(huì)拋出一個(gè)異常。
而在第二個(gè)任務(wù)中,我們先執(zhí)行任務(wù),再做取消,那么此時(shí)我們相當(dāng)于是在外部對(duì)此任務(wù)進(jìn)行取消控制,而且在執(zhí)行取消之后,任務(wù)2的狀態(tài)依然是RanToCompletion,而不是Canceled。因?yàn)閺腡PL的角度來看,該任務(wù)正常完成了它的工作,所以我們?cè)诰帉懘a時(shí)需要辨別這兩種情況,同時(shí)理解它在兩種情況下職責(zé)的不同。
處理異常
在普通情況下,我們通常使用try-catch代碼塊來處理異常,但在TPL中,最底層的異常會(huì)被封裝為一個(gè)AggregateException的通用異常,如果需要獲取真正的異常,則需要訪問InnerException屬性,相關(guān)實(shí)現(xiàn)如圖所示:
相關(guān)代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks;namespace TPLDemo {class Program{static void Main(string[] args){Task<int> task;try{task = Task.Run(() => TaskMethod("任務(wù)1", 2));int result = task.Result;Console.WriteLine($"結(jié)果為:{result}");}catch (Exception ex){Console.WriteLine($"發(fā)生異常:{ex}");}Console.WriteLine("----------------------------------------------------------------------------------------");Console.Read();}static int TaskMethod(string name, int seconds){Console.WriteLine($@"Task {name} 是一個(gè)正在線程id為 {Thread.CurrentThread.ManagedThreadId} 上運(yùn)行的任務(wù),是否為線程池線程:{Thread.CurrentThread.IsThreadPoolThread}");Thread.Sleep(TimeSpan.FromSeconds(seconds));throw new Exception("異常!");}} }運(yùn)行后結(jié)果如圖所示:
從代碼實(shí)現(xiàn)和運(yùn)行結(jié)果中,我們可以看出調(diào)用Task的Result屬性,會(huì)使得當(dāng)前線程等待直到該任務(wù)完成,并將異常傳播到當(dāng)前線程,因此我們可以通過catch捕獲到該異常,且該異常的類型為AggregateException,同時(shí)我們打印出的結(jié)果包含底層真正異常內(nèi)容。
但在TPL中,還有另外一種方式來處理異常,那就是使用Task的GetAwaiter和GetResult方法來獲取結(jié)果,相關(guān)代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks;namespace TPLDemo {class Program{static void Main(string[] args){Task<int> task;try{task = Task.Run(() => TaskMethod("任務(wù)1", 2));int result = task.Result;Console.WriteLine($"結(jié)果為:{result}");}catch (Exception ex){Console.WriteLine($"發(fā)生異常:{ex}");}Console.WriteLine("----------------------------------------------------------------------------------------");Console.WriteLine();try{task = Task.Run(() => TaskMethod("任務(wù)2", 2));int result = task.GetAwaiter().GetResult();Console.WriteLine($"結(jié)果為:{result}");}catch (Exception ex){Console.WriteLine($"發(fā)生異常:{ex}");}Console.WriteLine("----------------------------------------------------------------------------------------");Console.Read();}static int TaskMethod(string name, int seconds){Console.WriteLine($@"Task {name} 是一個(gè)正在線程id為 {Thread.CurrentThread.ManagedThreadId} 上運(yùn)行的任務(wù),是否為線程池線程:{Thread.CurrentThread.IsThreadPoolThread}");Thread.Sleep(TimeSpan.FromSeconds(seconds));throw new Exception("異常!");}} }運(yùn)行后結(jié)果如圖所示:
我們從結(jié)果中可以看出,在這種情況下,可以直接捕獲到底層異常,而無需再訪問InnerException屬性,原因是TPL模型會(huì)直接提取該異常進(jìn)行處理。
由上述兩種情況我們可以得出結(jié)論:如果你需要直接獲取并處理底層異常,那么請(qǐng)使用GetAwaiter和GetResult方法來獲取Task的結(jié)果,反之,則可直接使用Result屬性。
任務(wù)并行
我們?cè)谥暗氖纠?#xff0c;都是單獨(dú)創(chuàng)建任務(wù)并執(zhí)行,每個(gè)任務(wù)的執(zhí)行過程和結(jié)果都是獨(dú)立的。那么,如果我們需要多個(gè)任務(wù)并行,要怎么做呢?可以使用如下方式:
我們分別創(chuàng)建了三個(gè)任務(wù),但任務(wù)之間并不再是無關(guān)聯(lián)的關(guān)系,而是使用了Task.WhenAll與ContineWith來使得它們以某種方式關(guān)聯(lián)起來。
相關(guān)代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks;namespace TPLDemo {class Program{static void Main(string[] args){var firstTask = new Task<int>(() => TaskMethod("任務(wù)1", 3));var secondTask = new Task<int>(() => TaskMethod("任務(wù)2", 2));var whenAllTask = Task.WhenAll(firstTask, secondTask);whenAllTask.ContinueWith(x =>{Console.WriteLine($"任務(wù)1結(jié)果為:{x.Result[0]},任務(wù)2結(jié)果為:{x.Result[1]}");}, TaskContinuationOptions.OnlyOnRanToCompletion);firstTask.Start();secondTask.Start();Console.Read();}static int TaskMethod(string name, int seconds){Console.WriteLine($@"Task {name} 是一個(gè)正在線程id為 {Thread.CurrentThread.ManagedThreadId} 上運(yùn)行的任務(wù),是否為線程池線程:{Thread.CurrentThread.IsThreadPoolThread}");Thread.Sleep(TimeSpan.FromSeconds(seconds));return 42 * seconds;}} }運(yùn)行后結(jié)果如圖所示:
分析代碼及運(yùn)行結(jié)果,我們可以得知,在前兩個(gè)任務(wù)完成后,第三個(gè)任務(wù)才開始運(yùn)行,并且該任務(wù)的結(jié)果提供了一個(gè)結(jié)果數(shù)組,第一個(gè)元素是第一個(gè)任務(wù)的結(jié)果,第二個(gè)元素是第二個(gè)任務(wù)的結(jié)果,以此類推。
在TPL中,我們也可以創(chuàng)建另外一系列任務(wù),并使用Task.WhenAny的方式等待這些任務(wù)中的任何一個(gè)執(zhí)行完成。當(dāng)有一個(gè)任務(wù)完成時(shí),會(huì)從列表中移除該任務(wù)并繼續(xù)等待其他任務(wù)完成,直到列表為空為止。獲取任務(wù)的完成進(jìn)展情況,或在運(yùn)行任務(wù)時(shí)使用超時(shí),都可以使用Task.WhenAny方法。例如我們等待一組任務(wù)運(yùn)行,并且使用其中一個(gè)任務(wù)來記錄是否超時(shí),如果該任務(wù)先完成,那么我們只需取消其他還未完成的任務(wù)即可。
小結(jié)
我們?cè)谶@一篇中,講解了TPL的發(fā)展歷程和使用方式,對(duì)比APM和EAP模型,TPL顯得比較靈活且功能強(qiáng)大,支持取消、異常和并行等操作。
但TPL模型仍有它的不足之處
閱讀此類程序代碼時(shí),仍難以理解程序的實(shí)際執(zhí)行順序。
處理異常時(shí),不得不使用單獨(dú)的后續(xù)操作任務(wù)來處理在之前的異步操作中發(fā)生的錯(cuò)誤,導(dǎo)致了代碼比較分散,增加了復(fù)雜度。
所以為了解決這些問題,微軟直接從語言層面引入了更高級(jí)別的抽象,真正簡化了異步編程,使得編寫異步程序更為容易。那么它又是什么呢?它能為我們提供多少便利性呢?預(yù)知后事如何,且聽下回分解。
您的點(diǎn)贊和在看是我創(chuàng)作的最大動(dòng)力,感謝支持
公眾號(hào):wacky的碎碎念
知乎:wacky
總結(jié)
以上是生活随笔為你收集整理的浅谈.Net异步编程的前世今生----TPL篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅谈.Net异步编程的前世今生----A
- 下一篇: Dapr牵手.NET学习笔记:Actor