C#/.NET 異步,你也許不知道的5種用法
????async/await異步操作,是C#中非常驚艷的“語法糖”,讓異步編程變得優(yōu)美且傻瓜化到了不可思議的程度。就連JavaScript都借鑒了async/await語法,讓回調(diào)泛濫的JavaScript代碼變得很優(yōu)美。
????我之前錄制的.NET視頻教程已經(jīng)把a(bǔ)sync/await等基礎(chǔ)知識(shí)介紹了,這篇文章不再介紹那些基礎(chǔ)知識(shí),如果有對(duì)它們還不了解的朋友,請(qǐng)到在B站等平臺(tái)中搜索我的頻道“楊中科”查看。
????本篇文章只對(duì)在之前的視頻教程中沒有提到的幾點(diǎn)做講解。
用法1、控制并行執(zhí)行的任務(wù)數(shù)量
?????? 在項(xiàng)目開發(fā)的時(shí)候,有時(shí)候有很多任務(wù)需要異步執(zhí)行,但是為了避免同時(shí)執(zhí)行的異步任務(wù)太多,反而降低性能,因此通常需要限制并行執(zhí)行的任務(wù)的數(shù)量。比如爬蟲并行從網(wǎng)上抓取內(nèi)容的時(shí)候,就要根據(jù)情況限制最大執(zhí)行的線程的數(shù)量。
????在沒有async/await的年代,需要使用信號(hào)量等機(jī)制來進(jìn)行線程間通訊來協(xié)調(diào)各個(gè)線程的執(zhí)行,需要開發(fā)者對(duì)于多線程的技術(shù)細(xì)節(jié)非常了解。而使用async/await之后,這一切就可以變得非常傻瓜化了。
????比如下面的代碼用來首先從words.txt這個(gè)每行一個(gè)英文單詞的字典中,逐個(gè)讀取單詞,然后調(diào)用一個(gè)API接口來獲得單詞的“音標(biāo)、中文含義、例句”等詳細(xì)信息。為了加快處理速度,需要采用異步編程來實(shí)現(xiàn)多任務(wù)同時(shí)下載,但是又要限制同時(shí)執(zhí)行的任務(wù)的數(shù)量(假設(shè)為5個(gè))。實(shí)現(xiàn)代碼如下:
?核心代碼就是下面這一段:
????這里遍歷所有單詞,抓取單詞并且保存到磁盤的Process方法的返回值Task沒有使用await關(guān)鍵字進(jìn)行修飾,而是把返回的Task對(duì)象保存到list中,由于沒有使用await進(jìn)行等待,因此不用等一個(gè)任務(wù)執(zhí)行完成,就可以把下一個(gè)任務(wù)加入list。當(dāng)list中的任務(wù)滿五個(gè)的時(shí)候,就調(diào)用await Task.WhenAll(tasks);等待這五個(gè)任務(wù)執(zhí)行完成后,再處理下一組(5個(gè))。循環(huán)之外的await Task.WhenAll(tasks);的是用來處理最后一組不足5個(gè)任務(wù)的情況。
?用法2、在BackgroundService等異步執(zhí)行的代碼中進(jìn)行DI注入
?使用依賴注入(DI)的時(shí)候,注入的對(duì)象都是有生命周期的。比如使用services.AddDbContext<TestDbContext>(...);這種方式注入EF Core中的DbContext的時(shí)候,TestDbContext的生命周期就是scope。在普通的MVC的Controller中可以直接注入TestDbContext,但是在BackgroundService中是不能直接注入TestDbContext的。這時(shí)候,可以注入IServicescopeFactory對(duì)象,然后在使用到TestDbContext對(duì)象的時(shí)候再調(diào)用IServicescopeFactory的Createscope()方法來生成一個(gè)IServicescope,并且使用IServicescope的ServiceProvider來手動(dòng)解析獲取TestDbContext對(duì)象。
代碼如下:
?
?
用法3、異步方法可以不await
????我在做youzack背單詞的時(shí)候,有一個(gè)查詢單詞的功能。為了提升客戶端的響應(yīng)速度,我把每個(gè)單詞的明細(xì)信息都按照“每個(gè)單詞一個(gè)json文件”的形式,把單詞的詳細(xì)信息保存到文件服務(wù)器,相當(dāng)于做了一個(gè)“靜態(tài)化”。因此客戶端在查詢單詞的時(shí)候,先到文件服務(wù)器中查找一下是否有對(duì)應(yīng)的靜態(tài)文件,如果有的話,就直接加載靜態(tài)文件。如果在文件服務(wù)器不存在的話,再調(diào)用API接口的方法去查詢,API接口從數(shù)據(jù)庫中查詢到單詞后,不僅會(huì)把單詞的詳細(xì)信息返回給客戶端,而且還會(huì)把單詞的詳細(xì)信息再上傳到文件服務(wù)器。這樣以后客戶端再查詢這個(gè)單詞,就可以直接從文件服務(wù)器查詢了。
????因此API接口中“把從數(shù)據(jù)庫中查詢到的單詞的詳細(xì)信息上傳到文件服務(wù)器”這個(gè)操作對(duì)于接口的請(qǐng)求者來講沒什么意義,而且會(huì)降低接口的響應(yīng)速度,因此我就把“上傳到文件服務(wù)器”這個(gè)操作寫到了異步方法中,并且沒有通過await來等待。
????偽代碼如下:
????在上面的UploadAsync調(diào)用中沒有await調(diào)用等待,因此只要從數(shù)據(jù)庫中查詢出來,就把detail返回給請(qǐng)求者了,留下UploadAsync在異步線程中慢慢執(zhí)行。
????前面加的“_=”是消除對(duì)于不await異步方法造成編譯器警告。
用法4、異步代碼中Sleep的坑
????在編寫代碼的時(shí)候,有時(shí)候我們需要“暫停一段時(shí)間,再繼續(xù)執(zhí)行代碼”。比如調(diào)用一個(gè)Http接口,如果調(diào)用失敗,則需要等待2秒鐘再重試。
????在異步方法中,如果需要“暫停一段時(shí)間”,那么請(qǐng)使用Task.Delay(),而不是Thread.Sleep(),因?yàn)門hread.Sleep()會(huì)阻塞主線程,就達(dá)不到“使用異步提升系統(tǒng)并發(fā)能力”的目的了。
????如下代碼是錯(cuò)誤的:
上面的代碼是能夠正確的編譯執(zhí)行的,但是會(huì)大大降低系統(tǒng)的并發(fā)處理能力。因此要用Task.Delay()代替Thread.Sleep()。如下是正確的:
?
用法5、yield如何用到異步方法中
????yield由于可以實(shí)現(xiàn)“產(chǎn)生一個(gè)數(shù)據(jù)就讓IEnumerable的使用者處理一個(gè)數(shù)據(jù)”,從而實(shí)現(xiàn)數(shù)據(jù)處理的“流水線化”,提升數(shù)據(jù)處理的速度。
????但是,由于yield和async都是編譯器提供的語法糖,編譯器都會(huì)把它們修飾的方法編譯為一個(gè)使用了狀態(tài)機(jī)的類。因此兩個(gè)語法糖碰到一起,編譯器就迷惑了,因此不能直接在async修飾的異步方法中使用yield返回?cái)?shù)據(jù)。
????因此下面的代碼是錯(cuò)誤的:
????只要把IEnumerable改成IAsyncEnumerable就可以了,如下是正確的:
但是調(diào)用同時(shí)使用了async和yield的代碼,不能使用普通的foreach+await,如下是錯(cuò)誤的:
????需要把a(bǔ)wait關(guān)鍵詞移動(dòng)到在foreach之前,如下是正確的:
????編譯器是微軟寫的,不知道為什么不支持foreach (int i in await ReadCC())這樣的寫法,可能是由于為了兼容之前的C#語法規(guī)范不得已而為之吧。