關(guān)于C#異步編程你應(yīng)該了解的幾點(diǎn)建議


前段時(shí)間寫了一篇關(guān)于C#異步編程入門的文章,你可以點(diǎn)擊《C#異步編程入門看這篇就夠了》查看。這篇文章我們來(lái)討論下關(guān)于C#異步編程幾個(gè)不成文的建議,希望對(duì)你寫出高性能的異步編程代碼有所幫助。注:本文的很多內(nèi)容都是學(xué)習(xí)《Effective C#》的總結(jié)。
作者:依樂(lè)祝
原文地址:https://www.cnblogs.com/yilezhu/p/12099219.html
盡量不要編寫返回值類型為void的異步方法
在通常情況下,建議大家不要編寫那種返回值類型為void的異步方法,因?yàn)檫@樣做會(huì)破壞該方法的啟動(dòng)者與方法本身之間的約定,這套約定本來(lái)可以確保主調(diào)方能夠捕獲到異步方法所發(fā)生的異常。
正常的異步方法是通過(guò)它返回的Task對(duì)象來(lái)匯報(bào)異常的。如果執(zhí)行過(guò)程中發(fā)生了異常,那么Task對(duì)象就進(jìn)入了faulted(故障)狀態(tài)。主調(diào)方在對(duì)異步方法所返回的Task對(duì)象做await操作時(shí),該對(duì)象若已處在faulted狀態(tài),系統(tǒng)則會(huì)將執(zhí)行異步方法的過(guò)程中所發(fā)生的異常拋出,反之,若Task尚未執(zhí)行到拋出異常的那個(gè)地方,則主調(diào)方的執(zhí)行進(jìn)度會(huì)暫停在await語(yǔ)句這里,等系統(tǒng)稍后安排某個(gè)線程繼續(xù)執(zhí)行該語(yǔ)句下方的那些代碼時(shí),異常才會(huì)拋出。
總結(jié)一句話就是:
void的異步方法發(fā)生異常時(shí),開發(fā)者得不到任何通知,程序既不會(huì)觸發(fā)普通的異常處理程序,也不會(huì)把這些異常記錄下來(lái)??傊?,這會(huì)讓相關(guān)的線程默默的終止掉。
不要把同步方法與異步方法組合起來(lái)使用
用async關(guān)鍵字來(lái)修飾的方法意味著該方法有可能會(huì)在執(zhí)行完所有工作之前就把控制權(quán)返回給主調(diào)方,而且,它返回給主調(diào)方的是個(gè)代表工作進(jìn)度的Task對(duì)象。主調(diào)方可以查詢此對(duì)象的狀態(tài),以了解該工作是否已經(jīng)完成、尚未完成還是在執(zhí)行過(guò)程中發(fā)生了故障。此外,這種方法還在暗示主調(diào)方:本方法所執(zhí)行的工作可能要花費(fèi)很長(zhǎng)時(shí)間,因此建議你先去做其他一些事情,稍后再來(lái)向我索要結(jié)果。
與此相反,如果把某個(gè)方法設(shè)計(jì)成同步方法,那么意味著當(dāng)該方法執(zhí)行完畢時(shí),它的后置條件必定能夠得到滿足。無(wú)論這個(gè)方法要花多長(zhǎng)時(shí)間去完成工作,它都會(huì)采用與主調(diào)方相同的資源來(lái)完成,主調(diào)方必須等這個(gè)方法徹底執(zhí)行完畢才能向下執(zhí)行。
這兩種方法單獨(dú)寫起來(lái)都很清晰,但是如果把他們組合在一起就會(huì)讓方法變得十分難用,而且有可能導(dǎo)致各種bug,如死鎖。因此,這里提出兩條重要的原則。第一,不要讓同步方法必須等待異步方法執(zhí)行完畢才能往下執(zhí)行(盡量不用Wait()以及.result這些阻塞式的方法)。第二,不要讓異步方法把雖然耗時(shí)很長(zhǎng)、計(jì)算量很大但是完全可以由自己執(zhí)行的工作轉(zhuǎn)交給另一個(gè)異步任務(wù)去做?!?br style="margin: 0px;padding: 0px;">當(dāng)然對(duì)于第二點(diǎn),這并不是說(shuō)計(jì)算量較大的任務(wù)絕對(duì)不能放在單獨(dú)的線程中執(zhí)行,而是說(shuō)不應(yīng)該把只用一個(gè)線程就能迅速做好的任務(wù)刻意的拆解成許多個(gè)較小的部分,并把他們分別放在多個(gè)新的線程上執(zhí)行,而是應(yīng)該把整個(gè)任務(wù)都交給某個(gè)線程來(lái)執(zhí)行才對(duì)。
使用異步方法時(shí)應(yīng)盡量避免線程分配
異步任務(wù)看上去好像很神奇,因?yàn)檫@種任務(wù)刻意轉(zhuǎn)移到另一個(gè)地方去做,使得開啟這項(xiàng)任務(wù)的異步方法可以在該任務(wù)完成之后,從早前暫停的地方繼續(xù)往下推進(jìn)。不過(guò),要想發(fā)揮異步任務(wù)的功效,就必須保證把這項(xiàng)任務(wù)交出去確實(shí)能夠少占用一些資源,而不是僅僅會(huì)在相似的資源之間進(jìn)行上下文切換。
如:對(duì)于一個(gè)控制臺(tái)程序,如果只是執(zhí)行一項(xiàng)計(jì)算量較大且耗時(shí)較長(zhǎng)的任務(wù)(或者說(shuō),運(yùn)行時(shí)間較長(zhǎng)的CPU密集型的任務(wù)),那么把該任務(wù)單獨(dú)放在另一個(gè)線程中并沒(méi)有多大好處。因?yàn)檫@樣做只能讓工作線程始終處于繁忙狀態(tài),而主線程則必須一直卡在那里等待工作線程把任務(wù)做完。在這種情況下,實(shí)際上是用兩個(gè)線程來(lái)完成原本只需要一個(gè)線程就能做好的工作,造成了資源的浪費(fèi)。
避免不必要的上下文切換
目前C#代碼中使用async以及await實(shí)現(xiàn)的異步方法默認(rèn)是把await之后的代碼放在早前捕獲的那個(gè)上下文中執(zhí)行的,這是因?yàn)檫@樣做比較穩(wěn)妥,它最多只會(huì)引發(fā)幾次無(wú)謂的上下文切換,而不會(huì)使程序出現(xiàn)重大的錯(cuò)誤,與之相反,如果系統(tǒng)不把山下文切換回去,那么萬(wàn)一遇到的是只能在特定的上下文中才能執(zhí)行的代碼,那么程序就有可能崩潰。因此,無(wú)論有沒(méi)有必要切換上下文,系統(tǒng)都會(huì)切換至早前捕獲到的那個(gè)上下文,并把await之后的語(yǔ)句放在那個(gè)上下文執(zhí)行。
如果不想讓系統(tǒng)做出這樣的安排,那么可以調(diào)用ConfigureAwait()方法。這表示接下來(lái)的那些代碼無(wú)須放在早前捕獲的上下文中執(zhí)行。例如在很多程序集中,await語(yǔ)句之后的那些代碼一般都與上下文無(wú)關(guān),因此與,可以調(diào)用Task對(duì)象的ConfigureAwait()方法告訴系統(tǒng),在執(zhí)行完這項(xiàng)任務(wù)之后,不必專門把await下面的代碼放在早前捕獲的上下文中運(yùn)行。如下所示:
public static async Task ReadPacket(string url)
{
var result=await DownloadAsync(url)
.ConfigureAwait(false);
return XElement.Parse(result);
}C#語(yǔ)言默認(rèn)讓程序把await下面的語(yǔ)句都放在早前捕獲的上下文中執(zhí)行,這樣做雖然較為安全,但是會(huì)降低程序的效率。因此為了讓用戶能夠更加順暢的使用程序,我們應(yīng)該調(diào)整代碼的結(jié)構(gòu),把必須運(yùn)行在特定上下文的代碼剝離出來(lái),并盡量考慮在await語(yǔ)句那里調(diào)用ConfigureAwait(false),使得程序可以把語(yǔ)句下面的代碼放在默認(rèn)上下文中運(yùn)行,而不是切換回早前的上下文。
通過(guò)Task對(duì)象來(lái)進(jìn)行異步開發(fā)
Task(任務(wù))是一種抽象機(jī)制,可以用來(lái)表示某項(xiàng)工作,于是,就能夠把該工作轉(zhuǎn)交給其他資源去完成。Task類型以及與之相關(guān)的類與結(jié)構(gòu)體提供了豐富的API,讓開發(fā)者可以操控Task對(duì)象以及由該對(duì)象所表示的工作。此外,Task對(duì)象自身也具備一些方法與屬性,可以用來(lái)操作本對(duì)象所表示的任務(wù)。這些Task對(duì)象可以合起來(lái)構(gòu)成一項(xiàng)比較大的任務(wù),他們之間既能夠按照順序執(zhí)行,也能夠平行的執(zhí)行。
可以通過(guò)await語(yǔ)句來(lái)確保某些任務(wù)之間能夠按照一定的順序執(zhí)行,也就是說(shuō),只有當(dāng)該語(yǔ)句所要等待的那項(xiàng)工作完畢之后,語(yǔ)句下方的代碼才能夠執(zhí)行。
總之,由于C#提供了一套豐富的API,因此可以寫出相當(dāng)優(yōu)雅的算法來(lái)處理Task對(duì)象,并對(duì)這些對(duì)象所表示的任務(wù)進(jìn)行安排。對(duì)任務(wù)的用法理解的越透徹,寫出來(lái)的異步代碼越清晰。
這里簡(jiǎn)單說(shuō)明兩個(gè)常用的API:
WhenAll:會(huì)根據(jù)現(xiàn)有的一批任務(wù)創(chuàng)建出一項(xiàng)新的任務(wù),只有當(dāng)那批任務(wù)全部執(zhí)行完畢時(shí),這項(xiàng)新人物才能夠完成。對(duì)Task.WhenAll所返回的新任務(wù)進(jìn)行
await操作會(huì)獲得一份列表,早前的那些任務(wù)的執(zhí)行結(jié)果就位于該列表中。WhenAny:為了盡早的獲得某個(gè)結(jié)果,可能啟動(dòng)多項(xiàng)任務(wù),使得他們分別從不同的途徑去獲取該結(jié)果。只要其中有一項(xiàng)任務(wù)完成,你的目標(biāo)就達(dá)成了,針對(duì)這項(xiàng)需求,可以考慮使用
Task.WhenAny方法,并把自己所創(chuàng)建的那批任務(wù)傳進(jìn)去。對(duì)WhenAny方法所返回的Task對(duì)象進(jìn)行await操作可以獲取到一項(xiàng)任務(wù),它指的就是這批任務(wù)中最先執(zhí)行完畢的那項(xiàng)任務(wù)。
考慮實(shí)現(xiàn)任務(wù)的取消協(xié)議
異步任務(wù)的編程模型(也叫基于任務(wù)的異步編程模型)提供了標(biāo)準(zhǔn)的API,用來(lái)取消任務(wù)或者廣播任務(wù)的執(zhí)行進(jìn)度。雖然這些API是可選的,但如果某項(xiàng)任務(wù)確實(shí)能夠匯報(bào)其進(jìn)度,或者能夠予以取消,那就可以考慮用合適的辦法來(lái)實(shí)現(xiàn)這些API。
針對(duì)需要取消的任務(wù),我們可以通過(guò)CanclelationTokenSource對(duì)象來(lái)進(jìn)行取消操作。這種對(duì)象是一種起到中介作用的對(duì)象。該對(duì)象處在有可能發(fā)出取消請(qǐng)求的客戶代碼與支持取消功能的那項(xiàng)操作之間。
如果正在執(zhí)行的任務(wù)發(fā)現(xiàn)客戶端想要取消該操作,那么它就會(huì)通過(guò)ThrowIfCanclellationRequested()方法拋出TaskCanclledException異常,庸醫(yī)表示整個(gè)工作流程沒(méi)有能夠完全得到執(zhí)行。
此外,返回值類型為void類型的異步方法不應(yīng)該支持取消功能。
緩存泛型異步方法的返回值
可能你在進(jìn)行異步編程的時(shí)候?qū)Ξ惒椒椒ㄔO(shè)置的返回類型都是Task或者Task,然而有些時(shí)候把返回值類型設(shè)為Task可能會(huì)影響性能。如果某個(gè)循環(huán)或某段代碼需要頻繁的運(yùn)行,那么系統(tǒng)就有可能分配很多個(gè)Task對(duì)象,從而占用相當(dāng)多的資源。好在C#提供了一種新的類型,叫做ValueTask對(duì)象,他用起來(lái)比普通的Task更為高效。該類型是值類型,因此創(chuàng)建這種類型的對(duì)象時(shí),不需要再分配額外的空間。這個(gè)好處使得我們可以多創(chuàng)建一些這樣的對(duì)象,而不用擔(dān)心它會(huì)像Task對(duì)象那樣占據(jù)過(guò)多的資源。如果你的異步方法可以根據(jù)早前緩存起來(lái)的結(jié)果直接返回相應(yīng)的值,那么尤其應(yīng)該考慮把返回值類型設(shè)置為ValueTask。
其次,ValueTask提供了一個(gè)能夠接受Task參數(shù)的構(gòu)造函數(shù),這個(gè)構(gòu)造函數(shù)會(huì)在其內(nèi)部等候該Task的執(zhí)行結(jié)果。
總結(jié)
今天分享的內(nèi)容比較多,而且很多都比較難理解,不過(guò)確實(shí)是寫出高性能異步方法所必須要掌握的技巧。由于時(shí)間較短,因此也沒(méi)來(lái)得及通過(guò)代碼進(jìn)行講述,所以需要有一定的基礎(chǔ)才能看懂,不過(guò)還是希望對(duì)您有所幫助。
掃碼求關(guān)注
給我好看
您看此文用
?
?
·
?
秒,轉(zhuǎn)發(fā)只需1秒呦~

好看你就
點(diǎn)點(diǎn)
我

