浅谈.Net异步编程的前世今生----异步函数篇(完结)
前言
上一篇我們著重講解了TPL任務(wù)并行庫(kù),可以看出TPL已經(jīng)很符合現(xiàn)代API的特性:簡(jiǎn)潔易用。但它的不足之處在于,使用者難以理解程序的實(shí)際執(zhí)行順序。
為了解決這些問題,在C# 5.0中,引入了新的語言特性,被稱為異步函數(shù)(asynchronous function)。對(duì)應(yīng)的.Net版本為.Net Framework 4.5。
最后一個(gè)異步編程模型:異步函數(shù)
概述
由于異步函數(shù)為語言特性的實(shí)現(xiàn),因此它的本質(zhì)依然屬于TPL模型,但提供了更高級(jí)別的抽象,真正簡(jiǎn)化了異步編程。抽象可以隱藏主要的實(shí)現(xiàn)細(xì)節(jié),使得開發(fā)人員無需考慮許多重要的事情,從而達(dá)到簡(jiǎn)化的效果。
在本文中,我們主要會(huì)講解異步函數(shù)的聲明和使用方式,以及在多種場(chǎng)景下使用異步函數(shù),處理異常等。
聲明異步函數(shù)
聲明異步函數(shù)的方法很簡(jiǎn)單,只需使用async關(guān)鍵字標(biāo)注任意一個(gè)方法即可。需要注意的是,如果只使用了async標(biāo)注方法,而方法內(nèi)部未使用await,會(huì)導(dǎo)致編譯警告,如圖所示:
另一個(gè)重要的事實(shí)是,異步函數(shù)必須返回Task或Task<T>類型。也可使用async void,但不推薦,若使用async void方式, 異常處理及跟蹤將不由TPL模型處理,而是會(huì)直接在SynchronizationContext上引發(fā),這樣會(huì)引起整個(gè)進(jìn)程的崩潰。因此通常會(huì)在UI層處理事件時(shí),才會(huì)使用async void方式。
改寫后相關(guān)代碼示例如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace asyncDemo {public class Utils{public async Task<string> GetStringAsync(){await Task.Delay(TimeSpan.FromSeconds(2));return "Hello World!";}} }這里我們執(zhí)行完await調(diào)用的代碼行后,會(huì)立即返回,而不是阻塞兩秒,如果是同步執(zhí)行則結(jié)果相反。當(dāng)執(zhí)行完await操作后,TPL會(huì)立即將工作線程放回線程池,我們的程序會(huì)進(jìn)行異步等待。直到2秒后,我們又一次從線程池中得到工作線程,并繼續(xù)運(yùn)行其中剩余的異步方法。這樣就允許我們?cè)诘却?秒時(shí),可以重用工作線程來做其他事,提升了應(yīng)用程序的可伸縮性。
事實(shí)上,異步函數(shù)在編譯器后臺(tái)會(huì)被編譯成復(fù)雜的程序結(jié)構(gòu),一般稱之為迭代器。迭代器的內(nèi)部是一種狀態(tài)機(jī),由于狀態(tài)機(jī)的概念理解較為復(fù)雜,因此這里不再贅述。所以我們?cè)谌粘>帉懘a時(shí),并不需要將每一個(gè)方法都標(biāo)記為async,尤其是并不需要使用異步的方法。通過上述概念可知,濫用async會(huì)導(dǎo)致編譯器編譯時(shí)生成大量的迭代器,會(huì)有顯著的性能損失。
獲取異步任務(wù)結(jié)果
既然我們已經(jīng)了解了async-await本質(zhì)上依然為TPL模型,那么在使用TPL和await操作符獲取異步結(jié)果中有什么不同呢?此處我們可以通過實(shí)驗(yàn)來探究。
如圖所示,我們分別使用Task和await執(zhí)行:
二者都調(diào)用了同一個(gè)異步函數(shù)打印當(dāng)前線程的Id和狀態(tài)。
在第一個(gè)中啟動(dòng)了一個(gè)任務(wù),運(yùn)行2秒后返回關(guān)于工作線程的信息。我們還定義了一個(gè)后續(xù)操作,用于在異步操作完成后,打印出操作結(jié)果;另一個(gè)后續(xù)操作用于有錯(cuò)誤發(fā)生時(shí),打印異常信息。最終返回一個(gè)代表其中一個(gè)后續(xù)操作任務(wù)的任務(wù),并在Main中等待其執(zhí)行完成。
而在第二個(gè)中,我們直接使用await對(duì)任務(wù)進(jìn)行操作,獲取異步執(zhí)行的結(jié)果,同時(shí)使用try-catch代碼塊來捕獲可能發(fā)生的異常,這和我們編寫同步方法的代碼風(fēng)格是一致的,簡(jiǎn)化了程序編寫的復(fù)雜度。實(shí)際上在await之后編譯器創(chuàng)建了一個(gè)任務(wù)及后續(xù)操作,并處理了可能發(fā)生的異常信息。
相關(guān)代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks;namespace asyncDemo {class Program{static void Main(string[] args){Task t = AsyncTPL();t.Wait();t = AsyncAwait();t.Wait();Console.Read();}static Task AsyncTPL(){Task<string> t = GetInfoAsync("任務(wù)1");Task t2 = t.ContinueWith(x => Console.WriteLine(t.Result), TaskContinuationOptions.NotOnFaulted);Task t3 = t.ContinueWith(x => Console.WriteLine(t.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted);return Task.WhenAny(t2, t3);}async static Task AsyncAwait(){try{string result = await GetInfoAsync("任務(wù)2");Console.WriteLine(result);}catch (Exception ex){Console.WriteLine(ex);}}async static Task<string> GetInfoAsync(string name){await Task.Delay(TimeSpan.FromSeconds(2));return $"{name}的線程Id為:{Thread.CurrentThread.ManagedThreadId},是否為線程池線程:" +$"{Thread.CurrentThread.IsThreadPoolThread}";}} }運(yùn)行后,如圖所示:
從結(jié)果中我們可以看出,兩種操作的方式在概念上是等同的,但是第二種方式中編譯器隱式處理了異步相關(guān)的代碼,背后的邏輯更為復(fù)雜,我們?cè)诤罄m(xù)小節(jié)中會(huì)借助示例再詳細(xì)說明這些內(nèi)容。
多個(gè)連續(xù)的await
我們已經(jīng)得知了使用await的代碼行將會(huì)異步執(zhí)行,那么如果我們?cè)谕粋€(gè)async方法中使用多個(gè)連續(xù)的await,它們會(huì)并行異步執(zhí)行嗎?我們不妨一試。
如圖所示,我們依然定義TPL和Async函數(shù)進(jìn)行對(duì)比:
我們?cè)诙xAsyncAwait方法時(shí),依然使用同步代碼的方式進(jìn)行書寫,唯一的不同之處是連續(xù)使用了兩個(gè)await聲明。
而在TPL方法中,則使用了一個(gè)容器任務(wù),來處理所有相互依賴的任務(wù)。然后啟動(dòng)主任務(wù),并為其添加一系列的后續(xù)操作。當(dāng)該任務(wù)完成時(shí),會(huì)打印出其結(jié)果,然后再啟動(dòng)第二個(gè)任務(wù),并拋出一個(gè)異常,打印出異常信息。
相關(guān)代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks;namespace asyncDemo {class Program{static void Main(string[] args){Task t = AsyncTPL();t.Wait();t = AsyncAwait();t.Wait();Console.Read();}static Task AsyncTPL(){var continueTask = new Task(() =>{Task<string> t = GetInfoAsync("TPL1");t.ContinueWith(task =>{Console.WriteLine(t.Result);Task<string> t2 = GetInfoAsync("TPL2");t2.ContinueWith(innerTask =>Console.WriteLine(innerTask.Result),TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.AttachedToParent);t2.ContinueWith(innerTask =>Console.WriteLine(innerTask.Exception.InnerException),TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);},TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.AttachedToParent);t.ContinueWith(task =>Console.WriteLine(t.Exception.InnerException),TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);});continueTask.Start();return continueTask;}async static Task AsyncAwait(){try{string result = await GetInfoAsync("Async1");Console.WriteLine(result);result = await GetInfoAsync("Async2");Console.WriteLine(result);}catch (Exception ex){Console.WriteLine(ex);}}async static Task<string> GetInfoAsync(string name){Console.WriteLine($"{name} 開始執(zhí)行!");await Task.Delay(TimeSpan.FromSeconds(2));if (name == "TPL2"){throw new Exception("發(fā)生異常!");}return $"{name}的線程Id為:{Thread.CurrentThread.ManagedThreadId},是否為線程池線程:" +$"{Thread.CurrentThread.IsThreadPoolThread}";}} }運(yùn)行后,執(zhí)行結(jié)果如圖所示:
我們從結(jié)果中可以看出,TPL的后續(xù)依賴任務(wù)會(huì)按照我們的書寫順序依次執(zhí)行,讓人訝異的是await,它并沒有并行執(zhí)行,而也是順序執(zhí)行的。Async2任務(wù)只有等Async1任務(wù)完成后才會(huì)開始執(zhí)行,但它為什么是異步程序呢?
事實(shí)上,它并不總是異步的,當(dāng)使用await時(shí),如果一個(gè)任務(wù)已經(jīng)完成,我們會(huì)異步地得到相應(yīng)的任務(wù)結(jié)果。否則,在看到await聲明時(shí),通常的行為是方法執(zhí)行到await代碼行應(yīng)立即返回,且剩下的代碼會(huì)在一個(gè)后續(xù)操作任務(wù)中執(zhí)行。因此等待操作結(jié)果時(shí),并沒有阻塞程序執(zhí)行,這是一個(gè)異步調(diào)用。當(dāng)AsyncAwait方法中的代碼在執(zhí)行時(shí),除了可以在Main中執(zhí)行t.Wait外,我們可以執(zhí)行其他任何任務(wù)。但主線程必須等待直到所有異步操作完成,否則主線程完成后會(huì)停止所有異步操作的后臺(tái)線程。
這兩段代碼中,如果要比較TPL和await,那么則是TPL方法的書寫更容易閱讀和理解,調(diào)用層次更為清晰,請(qǐng)記住一點(diǎn),異步并不總是意味著并行執(zhí)行。
并行執(zhí)行的await
現(xiàn)在我們已經(jīng)得知了,異步并不總是并行的,那么它能不能通過某種手段或方式進(jìn)行并行操作呢?答案是可以的,我們一起看一下如何實(shí)現(xiàn):
這里我們定義了2個(gè)不同的Task分別運(yùn)行3秒和5秒,然后使用Task.WhenAll來創(chuàng)建另一個(gè)任務(wù),該任務(wù)只有在所有底層任務(wù)完成后才會(huì)執(zhí)行,之后我們等待所有任務(wù)的結(jié)果。
相關(guān)實(shí)現(xiàn)代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks;namespace asyncDemo {class Program{static void Main(string[] args){Task t = AsyncProcessing();t.Wait();Console.Read();}async static Task AsyncProcessing(){Task<string> t1 = GetInfoAsync("任務(wù)1", 3);Task<string> t2 = GetInfoAsync("任務(wù)2", 5);string[] results = await Task.WhenAll(t1, t2);foreach (string result in results){Console.WriteLine(result);}}async static Task<string> GetInfoAsync(string name, int seconds){await Task.Delay(TimeSpan.FromSeconds(seconds));//await Task.Run(() => Thread.Sleep(TimeSpan.FromSeconds(seconds)));return $"{name}的線程Id為:{Thread.CurrentThread.ManagedThreadId},是否為線程池線程:" +$"{Thread.CurrentThread.IsThreadPoolThread}";}} }運(yùn)行后,結(jié)果如圖所示:
根據(jù)程序運(yùn)行的結(jié)果我們可以看到,5秒之后,我們獲取到了所有的結(jié)果,說明這些任務(wù)是同時(shí)運(yùn)行的。這里還有一個(gè)有趣的現(xiàn)象是,兩個(gè)任務(wù)是被同一個(gè)線程池中的工作線程執(zhí)行的,為什么會(huì)這樣呢?這時(shí)候我們可以注釋掉Task.Delay這行代碼,并取消對(duì)Task.Run的注釋,再次運(yùn)行后,結(jié)果如圖所示:
此時(shí)我們會(huì)發(fā)現(xiàn),兩個(gè)任務(wù)會(huì)被不同的工作線程執(zhí)行。
造成這種情況的原因是Task.Delay在幕后使用了一個(gè)計(jì)時(shí)器,它的執(zhí)行過程如下:
1、從線程池中獲取工作線程,它將等待Task.Delay返回結(jié)果;
2、Task.Delay方法啟動(dòng)計(jì)時(shí)器,并指定一塊代碼,該代碼會(huì)在計(jì)時(shí)器到了Task.Delay中指定的時(shí)間后進(jìn)行調(diào)用,之后立即將工作線程返回線程池中;
3、當(dāng)計(jì)時(shí)器事件運(yùn)行時(shí)(類似于Timer類),我們會(huì)再次從線程池中獲取一個(gè)可用的工作線程并運(yùn)行計(jì)時(shí)器給它的代碼(可能會(huì)是我們之前使用過的工作線程)。
而Task.Run方法則不同,它的執(zhí)行過程如下:
1、從線程池中獲取工作線程,并將其阻塞幾秒鐘;
2、獲取第二個(gè)工作線程,也將其阻塞幾秒鐘。
在此過程中,兩個(gè)工作線程并無法做其他事,只能進(jìn)行等待操作,因此在某種程度上,這兩個(gè)工作線程是被浪費(fèi)掉了。
所以我們?cè)趯?shí)際使用時(shí),盡量使用Task.Delay的方式進(jìn)行并行操作,而不是使用Task.Run。
處理異常
在異步函數(shù)中,處理異常可以像同步代碼那樣使用try-catch去處理,但是在不同的場(chǎng)景下,也有不同的使用方式,下面我們一起來看看有哪些常見的使用場(chǎng)景,如圖所示:
我們分別定義了三種場(chǎng)景:單個(gè)異常、多個(gè)異常及多個(gè)異常的異常集合。相關(guān)實(shí)現(xiàn)代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks;namespace asyncDemo {class Program{static void Main(string[] args){Task t = AsyncProcessing();t.Wait();Console.Read();}async static Task AsyncProcessing(){Console.WriteLine("1、單個(gè)異常");try{string result = await GetInfoAsync("任務(wù)1", 2);Console.WriteLine(result);}catch (Exception ex){Console.WriteLine($"異常內(nèi)容:{ex}");}Console.WriteLine("-----------------------------------------------------");Console.WriteLine("2、多個(gè)異常");Task<string> t1 = GetInfoAsync("任務(wù)1", 3);Task<string> t2 = GetInfoAsync("任務(wù)2", 2);try{string[] results = await Task.WhenAll(t1, t2);Console.WriteLine(results.Length);}catch (Exception ex){Console.WriteLine($"異常內(nèi)容:{ex}");}Console.WriteLine("-----------------------------------------------------");Console.WriteLine("3、多個(gè)異常的異常集合");t1 = GetInfoAsync("任務(wù)1", 3);t2 = GetInfoAsync("任務(wù)2", 2);Task<string[]> t3 = Task.WhenAll(t1, t2);try{string[] results = await t3;Console.WriteLine(results.Length);}catch{var ae = t3.Exception.Flatten();var exceptions = ae.InnerExceptions;Console.WriteLine($"異常發(fā)生數(shù)量:{exceptions.Count}");}}async static Task<string> GetInfoAsync(string name, int seconds){await Task.Delay(TimeSpan.FromSeconds(seconds));throw new Exception($"異常來自于:{name}");}} }執(zhí)行后的結(jié)果如圖所示:
從執(zhí)行結(jié)果我們可以看出,如果在可能發(fā)生多個(gè)異常的場(chǎng)景下,仍直接使用try-catch的方式處理異常,那么只能從底層的AggregateException中獲取到第一個(gè)異常。
為了得到所有的異常信息,我們需要使用await任務(wù)的Exception屬性。在第三種場(chǎng)景中,我們使用了AggregateException的Flatten方法,將層級(jí)異常放入一個(gè)列表,從而達(dá)到獲取所有異常的效果,在實(shí)際使用時(shí)應(yīng)多加注意。
小結(jié)
至此為止,關(guān)于異步函數(shù)的特性及使用方式就已經(jīng)介紹完畢。通過異步模型的發(fā)展歷程我們可以看出,為了應(yīng)對(duì)不同時(shí)期的需求,異步模型也經(jīng)歷了由復(fù)雜到簡(jiǎn)單的過程。最終我們使用的異步函數(shù)模式,可以使得程序在編寫代碼時(shí),能用編寫同步代碼的方式來實(shí)現(xiàn)異步,大大降低了復(fù)雜度,也提升了代碼可讀性。由于該思想和語法相當(dāng)簡(jiǎn)潔,在其他語言中也借鑒了類似的語法,如JavaScript在ES6標(biāo)準(zhǔn)中也引入了async-await的寫法來實(shí)現(xiàn)異步,避免了多個(gè)回調(diào)嵌套的尷尬方式。
但關(guān)于async-await本身,C#編譯器在背后通過及其復(fù)雜的原理為我們屏蔽了底層的細(xì)節(jié),包括為何不能使用async void等等,這些原理還是建議大家有時(shí)間的話進(jìn)行一些挖掘和探究,學(xué)習(xí)背后的設(shè)計(jì)思想,會(huì)對(duì)我們的程序設(shè)計(jì)思維大有裨益。
.Net異步編程系列的文章,到此也暫時(shí)告一段落了。我個(gè)人在后面的日子中也會(huì)將主要精力投入到架構(gòu)設(shè)計(jì)和微服務(wù)等前沿技術(shù)中,同時(shí)會(huì)總結(jié)一些個(gè)人的心得與體會(huì)形成其他系列的分享,請(qǐng)大家拭目以待。也感謝所有閱讀此系列文章的讀者,感謝大家的反饋,陪伴我度過一段難忘的時(shí)光,我們下一期再會(huì)!
參考
1.避免 Async Void?https://docs.microsoft.com/zh-cn/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
總結(jié)
以上是生活随笔為你收集整理的浅谈.Net异步编程的前世今生----异步函数篇(完结)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Envoy实现.NET架构的网关(四)集
- 下一篇: 双11,2分钟狂挣20亿的神秘大厂,急招