Runtime Async - 步入(ru)高性能異步時代
同步代碼和異步代碼
一般而言,代碼(ma)可(ke)分為同(tong)步(bu)與異步(bu)兩類。兩者同(tong)樣需要等待(dai)(dai)操作(zuo)(zuo)完(wan)成(cheng):同(tong)步(bu)會阻塞當前(qian)線(xian)程(cheng),直至操作(zuo)(zuo)結束后(hou)再繼(ji)續執行后(hou)續邏(luo)(luo)輯;異步(bu)則不阻塞當前(qian)線(xian)程(cheng),而是在發起操作(zuo)(zuo)時預(yu)先(xian)注(zhu)冊完(wan)成(cheng)后(hou)的處理邏(luo)(luo)輯,待(dai)(dai)操作(zuo)(zuo)完(wan)成(cheng)時由操作(zuo)(zuo)本(ben)身或外(wai)部機制觸發該邏(luo)(luo)輯。
于是(shi)這就帶(dai)來(lai)一個問題(ti),那就是(shi)同步(bu)代(dai)碼(ma)(ma)和異步(bu)代(dai)碼(ma)(ma)的寫法(fa)是(shi)完全不同的!
在 async/await 之前,異(yi)步編程(cheng)通常將回(hui)調(diao)函數交(jiao)給(gei)異(yi)步操作,以便在完成時觸發預先編寫的邏輯。其后果是:邏輯被(bei)拆(chai)散(san)到各個回(hui)調(diao)中,或層層嵌套成“回(hui)調(diao)地獄”。此外,回(hui)調(diao)必須由調(diao)用(yong)方(fang)向被(bei)調(diao)用(yong)方(fang)傳遞,迫使調(diao)用(yong)方(fang)提(ti)前了(le)解并攜帶(dai)完成后要(yao)喚醒(xing)的代碼(ma)(ma),這(zhe)與(yu)自然(ran)的思維方(fang)式相悖(bei)——同一(yi)項操作的完成可能會被(bei)多個位置同時關心(xin),而(er)發起該操作的代碼(ma)(ma)不應對等待其完成的代碼(ma)(ma)產生任何形式的依賴(lai)。
async/await 的出現則(ze)從根本上改變了這一(yi)點。
async/await
現如(ru)今我(wo)們提到 async/await,盡管(guan)它仍(reng)歸入 stackless coroutine 范疇,但已(yi)不(bu)同于早(zao)期那種在(zai)(zai)遞(di)歸、錯誤處理與調用棧追蹤上(shang)(shang)局限頗(po)多(duo)的形態;這些局限在(zai)(zai)很大程(cheng)度上(shang)(shang)已(yi)經被克服。
.NET 對 async/await 的支持,本質上(shang)是編(bian)譯器對異(yi)步方法進行一(yi)種 CPS 風格(ge)的變換,并將其落地為(wei)可恢復的狀態(tai)機(ji)。
舉一個具(ju)體(ti)的例(li)子,當(dang)遇到如下(xia)代碼(ma)時:
async Task Foo()
{
A();
await B();
C();
await E();
F();
}
編譯器(qi)會以(yi) await 為切分(fen)點生成(cheng)若干“續(xu)(xu)(xu)體”(continuation),并為每個續(xu)(xu)(xu)體捕獲(huo)所需的(de)局部變量與執行上下(xia)文,使其既(ji)可被(bei)獨立調(diao)度(du)執行,同時(shi)仍能訪(fang)問 await 之前的(de)狀(zhuang)態。這樣(yang)一來,只需在被(bei)等待的(de)操作(zuo)完成(cheng)時(shi)將(jiang)下(xia)一個續(xu)(xu)(xu)體交給(gei)調(diao)度(du)器(qi),就可以(yi)按自(zi)定(ding)義(yi)策略自(zi)由地推進后(hou)續(xu)(xu)(xu)代碼的(de)執行。異步方(fang)法(fa)在執行到每一處 await 時(shi)會被(bei)暫停,等待后(hou)續(xu)(xu)(xu)邏輯(ji)被(bei)重新調(diao)度(du)繼續(xu)(xu)(xu)執行。因此,await 實際上也標注(zhu)了(le)異步方(fang)法(fa)的(de)潛(qian)在暫停點。
在 C# 的第(di)一版 async/await 中,這一機制(zhi)具體抽(chou)象為(wei)編譯期生成的狀態(tai)機(實現 IAsyncStateMachine),由調度器/同步上下文驅動 MoveNext 逐(zhu)步推進,從而保(bao)證(zheng)每個代碼片段在前一個異步操作完成后被正確調度執行。
然(ran)(ran)(ran)而(er)一直以(yi)來 C# 的(de) async/await 實現都(dou)存在一個邊界(jie)(jie)(jie)上(shang)的(de)問題:C# 編譯器以(yi)方(fang)(fang)(fang)(fang)法(fa)(fa)為(wei)編譯單位,既無法(fa)(fa)跨越方(fang)(fang)(fang)(fang)法(fa)(fa)邊界(jie)(jie)(jie)全面洞察被調(diao)用(yong)(yong)(yong)方(fang)(fang)(fang)(fang)法(fa)(fa)的(de)實現細節,也不會(hui)改變 managed ABI 去(qu)擅自(zi)(zi)修改當前方(fang)(fang)(fang)(fang)法(fa)(fa)的(de)簽名。因此,在形(xing)成異(yi)步(bu)調(diao)用(yong)(yong)(yong)鏈時,通常(chang)每(mei)個 async 方(fang)(fang)(fang)(fang)法(fa)(fa)都(dou)會(hui)擁有自(zi)(zi)己的(de)狀態(tai)機(ji);而(er)在缺乏跨邊界(jie)(jie)(jie)全量信息的(de)情(qing)(qing)況(kuang)下(xia),調(diao)用(yong)(yong)(yong)方(fang)(fang)(fang)(fang)會(hui)生成較為(wei)通用(yong)(yong)(yong)的(de)路徑來覆蓋異(yi)常(chang)與暫停等情(qing)(qing)形(xing)。舉(ju)例來說,即便目(mu)標(biao)方(fang)(fang)(fang)(fang)法(fa)(fa)在多數情(qing)(qing)況(kuang)下(xia)并不會(hui)拋(pao)出(chu)異(yi)常(chang),調(diao)用(yong)(yong)(yong)點仍會(hui)保留(liu)異(yi)常(chang)捕獲(huo)與恢(hui)復路徑;又或者目(mu)標(biao)方(fang)(fang)(fang)(fang)法(fa)(fa)很可能不會(hui)暫停,調(diao)用(yong)(yong)(yong)點也會(hui)保留(liu)相(xiang)應的(de)暫停/恢(hui)復分(fen)支以(yi)保證語義正確;又或者比(bi)如異(yi)步(bu)調(diao)用(yong)(yong)(yong)鏈中每(mei)一處異(yi)步(bu)調(diao)用(yong)(yong)(yong)都(dou)通過 await 對其結(jie)(jie)果直接進(jin)行等待(dai),這種情(qing)(qing)況(kuang)下(xia)實際(ji)上(shang)并不需(xu)要(yao)將(jiang)異(yi)步(bu)操作的(de)結(jie)(jie)果包(bao)裝進(jin) Task 之類的(de)類型(xing),然(ran)(ran)(ran)而(er)由于(yu)需(xu)要(yao)保持(chi) managed ABI,編譯器仍然(ran)(ran)(ran)需(xu)要(yao)將(jiang)每(mei)一步(bu)的(de)結(jie)(jie)果包(bao)裝進(jin) Task 里面去(qu);再(zai)比(bi)如對于(yu)實際(ji)上(shang)沒有同步(bu)上(shang)下(xia)文(wen)的(de)情(qing)(qing)況(kuang),編譯器仍然(ran)(ran)(ran)需(xu)要(yao)產生備份/恢(hui)復同步(bu)上(shang)下(xia)文(wen)的(de)代碼。
上面的(de)問題使得編譯后的(de) C# 代(dai)碼難以被(bei) JIT 優(you)化,同時(shi)還會產生多余的(de) Task 對(dui)象分配(pei),從(cong)而導致 C# 中異步(bu)(bu)代(dai)碼的(de)性能一直無法與(yu)同步(bu)(bu)代(dai)碼相(xiang)匹敵,甚至(zhi)出現 ValueTask 這種專門(men)為了消除分配(pei)而誕(dan)生的(de)類型。
.NET 團隊自從 .NET 8 開始嘗試對(dui)這一(yi)現狀進(jin)行改(gai)進(jin)。先是對(dui) Green Thread 方(fang)案(an)(an)(與 goroutine、Java 的(de)(de) Virtual Thread 方(fang)案(an)(an)相同(tong))進(jin)行實驗(yan),結果相比目前(qian)的(de)(de) async/await 不僅性(xing)能沒(mei)有提(ti)升,反而在跨 runtime 邊(bian)界調用場景存在不可接受的(de)(de)性(xing)能回退和調度問題。在結束這一(yi)失敗的(de)(de)實驗(yan)之后,從 .NET 9 開始遍全(quan)(quan)力向著(zhu)改(gai)進(jin) async/await 本身的(de)(de)方(fang)向探(tan)索,于(yu)是,全(quan)(quan)新的(de)(de) Runtime Async 到(dao)來(lai)了。順帶(dai)一(yi)提(ti),Runtime Async 最早的(de)(de)名字叫做 Async 2。
Runtime Async
Runtime Async 下,我們需要編寫的 C# 代(dai)(dai)(dai)碼(ma)不能說(shuo)沒有一(yi)點變(bian)化,只能說(shuo)是一(yi)點變(bian)化沒有,只需要用支持 Runtime Async 的新(xin) C# 編譯(yi)器重新(xin)把代(dai)(dai)(dai)碼(ma)編譯(yi)一(yi)下,代(dai)(dai)(dai)碼(ma)中的老 Async 代(dai)(dai)(dai)碼(ma)就會被自(zi)動升級為新(xin)的 Async 代(dai)(dai)(dai)碼(ma),因此并不存在任何的源代(dai)(dai)(dai)碼(ma)破壞(huai)性更改。不過未經重新(xin)編譯(yi)的程序集不會自(zi)動升級到新(xin)的 Runtime Async 上去(qu)。
與依賴 C# 編(bian)譯(yi)器進行 CPS 變(bian)換的老 Async 實(shi)現相比(bi),新(xin)的 Runtime Async 并(bing)不需要(yao)編(bian)譯(yi)器改寫方法體(ti),而是在(zai) runtime 層面引(yin)入(ru)全新(xin)的 async ABI,由運(yun)行時(shi)直接承載與處理異步控制流。
在 Runtime Async 中,一個方法通過標注 async 這(zhe)一 attribute(注(zhu)意不是我們平常(chang)使用的(de) attribute,而是一種直(zhi)接進入方法簽名(ming)的(de)特殊 attribute)來表示自己遵(zun)循(xun)異步(bu)方法的(de) ABI。
比如,假設我們有以(yi)下代碼:
async Task Test()
{
await Test();
}
扔給(gei)老的(de)(de) C# 編(bian)譯器編(bian)譯則(ze)會(hui)得到一個狀態(tai)機;而(er)扔給(gei)新的(de)(de)啟(qi)用(yong)了 Runtime Async 支持的(de)(de) C# 編(bian)譯器編(bian)譯,則(ze)會(hui)得到如下 IL:
.method public hidebysig
instance class [System.Runtime]System.Threading.Tasks.Task Test() cil managed async
{
ldarg.0
call instance class [System.Runtime]System.Threading.Tasks.Task Program::Test()
call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
ret
}
狀態(tai)機完全(quan)消失了,取(qu)而(er)代(dai)之的只剩(sheng)下一個參考實現里面調(diao)用了一些 runtime helper 函數,以及我(wo)們 IL 代(dai)碼的方(fang)法簽名上那一個顯著的 async 標記(ji)。
以及,我(wo)們給(gei)方(fang)法返回值類型(xing)上寫的(de)(de)(de) Task 類型(xing)只不(bu)(bu)過是一個(ge)(ge)參(can)考,運行的(de)(de)(de)時(shi)候 runtime 并(bing)不(bu)(bu)一定會(hui)實(shi)際(ji)為(wei) Task 類型(xing)產(chan)生代(dai)(dai)碼(ma)(ma)(ma)。并(bing)且我(wo)們的(de)(de)(de) C# 代(dai)(dai)碼(ma)(ma)(ma)被(bei)(bei)(bei)編(bian)譯到(dao) IL 后,IL 代(dai)(dai)碼(ma)(ma)(ma)也只不(bu)(bu)過是一個(ge)(ge)參(can)考實(shi)現而(er)已,并(bing)不(bu)(bu)是會(hui)被(bei)(bei)(bei)真正執行的(de)(de)(de)代(dai)(dai)碼(ma)(ma)(ma)。實(shi)際(ji)真正被(bei)(bei)(bei)執行的(de)(de)(de)代(dai)(dai)碼(ma)(ma)(ma)則并(bing)沒有對應的(de)(de)(de) IL 表示(shi)形(xing)式,而(er)我(wo)們寫的(de)(de)(de)這個(ge)(ge) C# 函(han)數只不(bu)(bu)過是要被(bei)(bei)(bei)執行的(de)(de)(de)真實(shi)代(dai)(dai)碼(ma)(ma)(ma)的(de)(de)(de) trunk,或(huo)者叫它“啟動器”,在異步調用鏈中實(shi)際(ji)上并(bing)不(bu)(bu)存在。
在(zai)新(xin)的異(yi)步(bu)(bu)模(mo)型中,當(dang)在(zai)一個(ge)異(yi)步(bu)(bu)方法里(li)等待另一個(ge)異(yi)步(bu)(bu)方法時(shi),JIT 會(hui)(hui)生成暫(zan)停(ting)邏輯并把當(dang)前狀態捕獲到(dao)一個(ge) continuation 對象中;當(dang)需要“傳遞”暫(zan)停(ting)時(shi),則(ze)返回一個(ge)非空的 continuation。調(diao)用方收(shou)到(dao)非空 continuation 后,會(hui)(hui)相應地暫(zan)停(ting)自(zi)身、創建自(zi)己(ji)的 continuation 并返回。由此(ci)形成一條按照(zhao)調(diao)用層次串接起來的 continuation 鏈式結構。
恢(hui)復執(zhi)行(xing)時,通過參(can)數(shu)傳入一個非空的 continuation,根(gen)據其(qi)中記錄的暫停點(可理解為(wei)恢(hui)復點標識(shi))跳轉(zhuan)到相應位置繼(ji)續執(zhi)行(xing);若傳入的 continuation 為(wei)空,則(ze)表示(shi)從方法開(kai)頭開(kai)始執(zhi)行(xing)。
你(ni)會(hui)發現(xian)這一實現(xian)中,我們付出的(de)額外開銷僅僅只(zhi)有判斷 continuation 對象(xiang)是(shi)否是(shi) null 的(de)成(cheng)本(ben),這簡(jian)直可(ke)以忽略不計!
借助這一機制,runtime 可以在不受 managed ABI 限制的前提下跨(kua)越方法進行(xing)更積極的全(quan)局優(you)化:
- 被調用的異步方法不會拋異常?異常處理路徑刪了!
- 沒使用同步上下文?備份/恢復相關邏輯刪了!
- 實際不發生暫停?暫停/恢復分支跳了!
- 未在后續使用的局部變量?提前結束變量生命周期釋放內存!
- ...
同時,在許多異步(bu)(bu)(bu)等(deng)待鏈中(zhong),結果(guo)并不需(xu)要顯式(shi)由 Task 進(jin)行包裝,因此可(ke)以在整條(tiao)鏈路上(shang)徹底消除 Task 抽(chou)象:JIT 生成代碼(ma)時可(ke)以直(zhi)接(jie)傳遞(di)結果(guo)本身而(er)非 Task,從而(er)在熱路徑上(shang)實(shi)現零分配(pei)或(huo)接(jie)近(jin)零分配(pei)的效果(guo)。除此之外,這(zhe)還使得 JIT 有能(neng)力完全 inline 掉異步(bu)(bu)(bu)方法(fa),從而(er)進(jin)一(yi)步(bu)(bu)(bu)帶(dai)來大量的性能(neng)提升。
Runtime Async 在大(da)量場景中(zhong)顯著提升(sheng)了(le)(le)異步代碼的性能,使其(qi)逼近甚至達到(dao)(dao)同步代碼的性能,并有(you)效降低了(le)(le)分配和(he)內存占用,減少(shao)了(le)(le) GC 壓力;同時(shi) Runtime Async 還(huan)不會對跨 runtime 邊界(jie)的互操(cao)作(zuo)與任務調度帶來負面影響,可以說成功做到(dao)(dao)了(le)(le)既(ji)要還(huan)要。
染色問題?
當然,每當談起 async/await 的(de)時候,就會有復讀機復讀“染色(se)問(wen)題(ti)”。這種“問(wen)題(ti)”之所以(yi)存在,其實是因為同(tong)一(yi)套代(dai)碼需要同(tong)時承載同(tong)步與異步兩種語(yu)義。
若完全(quan)采用(yong)(yong)回調式異(yi)(yi)步(bu),容(rong)易導(dao)致邏輯分散(san)、可讀(du)性下降、維護成本上(shang)升(sheng),也(ye)不太符合(he)直覺;而如果全(quan)面(mian)協(xie)程化(hua)(如 goroutine),在(zai)(zai)異(yi)(yi)步(bu) runtime 內部(bu)通(tong)(tong)常(chang)表現良好,但在(zai)(zai)跨越 runtime 邊界與原生(sheng)世界交(jiao)互(如 FFI)時,就會在(zai)(zai)性能與調度(du)(du)上(shang)面(mian)臨很大的(de)挑(tiao)戰(zhan):原生(sheng)庫通(tong)(tong)常(chang)默(mo)認以系(xi)統線(xian)程為邊界模型,因此當跨邊界調用(yong)(yong)發生(sheng)阻塞時,runtime 往往需要(yao)避免(mian)在(zai)(zai)同(tong)(tong)一線(xian)程上(shang)繼續安(an)排其他任務,從而導(dao)致額外(wai)的(de)開銷;同(tong)(tong)時,由于調度(du)(du)行(xing)為與 runtime 緊密耦合(he),開發者(zhe)通(tong)(tong)常(chang)較(jiao)難(nan)精(jing)確控制代(dai)碼運行(xing)所在(zai)(zai)的(de)具(ju)體系(xi)統線(xian)程,遇到來自外(wai)部(bu)的(de)反向回調時也(ye)不易回到原先的(de)線(xian)程,進而在(zai)(zai)客戶端和(he)(he)游(you)戲等對線(xian)程親和(he)(he)性敏感的(de)場景中水土不服。
async/await 的(de)思路則是“看起來像(xiang)同步(bu)”的(de)方(fang)式(shi)編寫異(yi)步(bu),同時(shi)讓異(yi)步(bu)走(zou)有(you)別(bie)于同步(bu)的(de) ABI。它(ta)既能保留回調(diao)式(shi)的(de)性(xing)能優勢(shi),同時(shi)還具備(bei)完整(zheng)的(de)調(diao)度靈(ling)活(huo)性(xing),又有(you)助于降低維護成(cheng)本。然而(er)主要(yao)代價在于需要(yao)將結果包裝為 Task 等異(yi)步(bu)類型(xing),這就是人們(men)所說(shuo)的(de)“染色”,即異(yi)步(bu)類型(xing)沿調(diao)用(yong)鏈傳播。從(cong)抽象上(shang)看,可以視(shi)作以 Monad 的(de)方(fang)式(shi)對異(yi)步(bu)進(jin)行(xing)建模(mo),從(cong)而(er)允許同一異(yi)步(bu)結果被多方(fang)同時(shi)等待的(de)同時(shi),還能支持在異(yi)步(bu)操作結束之后隨時(shi)訪(fang)問(wen)異(yi)步(bu)操作的(de)結果。
因(yin)此從這(zhe)一點上來(lai)看,async/await 通常能(neng)在(zai)(zai)性(xing)能(neng)、可維護(hu)性(xing)與互(hu)操作性(xing)之間取得(de)較為(wei)理想的(de)(de)平衡:書寫與調試體驗接(jie)近同步代碼,組合能(neng)力(如超時(shi)(shi)、取消、WhenAll/WhenAny)完善;同時(shi)(shi)借助 Task 與同步上下文/調度器(qi),在(zai)(zai)需要時(shi)(shi)可以對(dui)線(xian)程(cheng)親(qin)和性(xing)進行更精細的(de)(de)控制,并為(wei)跨 FFI 的(de)(de)調用保留清(qing)晰的(de)(de)邊界(jie)。也正因(yin)此它在(zai)(zai)工程(cheng)實踐中被(bei) C++、C#、F#、Rust、Kotlin、JavaScript、Python 等語(yu)言廣泛采用。
開啟方法
從 .NET 10 RC1 開(kai)始,Runtime Async 已經作為(wei)實驗性預(yu)覽特性發布了出(chu)來(lai),因此想(xiang)要試(shi)用 Runtime Async 的開(kai)發者可以搶(qiang)先體驗。
不過需要(yao)提前說明(ming)的(de)是(shi),現(xian)階段 Runtime Async 仍然(ran)處(chu)于實(shi)(shi)驗性(xing)預覽階段,存在一(yi)些 bug,還(huan)不適合在實(shi)(shi)際的(de)生(sheng)產環境中使用(yong)。另(ling)外,標準庫(ku)(ku)也(ye)還(huan)沒有(you)采(cai)用(yong) Runtime Async 重新進行編譯(yi),因(yin)(yin)(yin)此 Runtime Async 只對你(ni)自己寫的(de)異(yi)步代碼生(sheng)效,而(er)調用(yong)進標準庫(ku)(ku)里的(de)異(yi)步代碼后仍然(ran)走的(de)是(shi)老(lao)(lao)的(de) Async 實(shi)(shi)現(xian)。此外,不少(shao)優化也(ye)還(huan)沒有(you)實(shi)(shi)裝(zhuang),因(yin)(yin)(yin)此現(xian)階段的(de)性(xing)能表現(xian)雖然(ran)已(yi)經比老(lao)(lao)的(de) Async 好了(le)一(yi)大(da)截,但(dan)離正式版的(de) Runtime Async 還(huan)差了(le)很遠(yuan)。另(ling)外雖然(ran)計劃支持 NativeAOT 但(dan)是(shi)因(yin)(yin)(yin)為工期不夠目前還(huan)沒有(you)實(shi)(shi)裝(zhuang)。
那么說了這么多,到底如何(he)在(zai) .NET 10 中提前體驗 Runtime Async 呢?
首(shou)先(xian)我(wo)(wo)們需要修改我(wo)(wo)們的(de) C# 項目(mu)文件,啟用(yong)預覽(lan)功能,并開啟 C# 編譯器的(de) Runtime Async 特性支(zhi)持:
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<Features>$(Features);runtime-async=on</Features>
<NoWarn>SYSLIB5007</NoWarn>
<LangVersion>preview</LangVersion>
</PropertyGroup>
然后我們需要設置環境變量 DOTNET_RuntimeAsync=1 開啟 runtime 層面的支持。
這樣我(wo)們就可(ke)以體驗 Runtime Async 帶(dai)來的提升(sheng)了(le)!
簡單測試
這里我們編寫一個遞歸計算斐波那契數列(lie)的方法,但(dan)是 async 版(ban)本:
class Program
{
static async Task Main()
{
// 把 Fib 和 FibAsync 預熱到 tier 1
for (var i = 0; i < 100; i++)
{
Fib(30);
await FibAsync(30);
await Task.Delay(1);
}
// 進行測試
var sw = Stopwatch.StartNew();
var result = Fib(40);
sw.Stop();
Console.WriteLine($"Fib(40) = {result} in {sw.ElapsedMilliseconds}ms");
sw.Restart();
result = await FibAsync(40);
sw.Stop();
Console.WriteLine($"FibAsync(40) = {result} in {sw.ElapsedMilliseconds}ms");
}
static async Task<int> FibAsync(int n)
{
if (n <= 1) return n;
return await FibAsync(n - 1) + await FibAsync(n - 2);
}
static int Fib(int n)
{
if (n <= 1) return n;
return Fib(n - 1) + Fib(n - 2);
}
}
使用 dotnet run -c Release 運行后得到結果:
Fib(40) = 102334155 in 250ms
FibAsync(40) = 102334155 in 730ms
而(er)老的(de) Async 結果(guo)長這樣:
FibAsync(40) = 102334155 in 1412ms
可以看(kan)到新的 Runtime Async 相(xiang)比老的 Async 在(zai)這一測試上直接成績暴(bao)漲 100%。
其實這(zhe)還(huan)并不(bu)是最(zui)終我們會看到(dao)的(de)(de)(de)成績。正如前(qian)面所說(shuo),在(zai) .NET 10 中一部分針對 Runtime Async 的(de)(de)(de)優化其實因為(wei)還(huan)存(cun)在(zai) bug 被(bei)臨時(shi)關閉了。我在(zai)這(zhe)些優化被(bei)關閉之前(qian)的(de)(de)(de)時(shi)候自(zi)己(ji)編譯源碼(ma)測(ce)試過一次 Runtime Async 性能,得到(dao)的(de)(de)(de)測(ce)試結果如下:
FibAsync(40) = 102334155 in 255ms
是的你沒有看錯,在這個測試中異步代碼成功做到了和同步代碼同樣的性能,甚至還是在有這么多層遞歸的情況之下,以及我們連 ValueTask 都沒使(shi)用。它相(xiang)比老的 Async 而言(yan)直(zhi)接(jie)提(ti)升(sheng)了(le)接(jie)近 500%!
當然,在真實(shi)世界的(de)(de)重 I/O 應(ying)用(yong)場景里,大量的(de)(de)時間其實(shi)都消耗在了真實(shi)的(de)(de) I/O 操作本(ben)身上(shang),因此總體(ti)上(shang)并(bing)不(bu)會有這么夸張的(de)(de)提(ti)升。不(bu)過對于想(xiang)要使(shi)用(yong) async/await 來做并(bing)行計算(suan)的(de)(de)同學來說,Runtime Async 可以說是給你們鋪(pu)平(ping)了道路(lu)。
結尾
Runtime Async 作為 .NET 全新的(de)異(yi)步(bu)方案,在(zai)保(bao)留源代碼兼(jian)容性(xing)(xing)的(de)同時,通(tong)過把 async 的(de)實現(xian)從編(bian)譯器搬到 runtime,已經(jing)展(zhan)示出可觀的(de)性(xing)(xing)能(neng)(neng)改善。對于大規模(mo)異(yi)步(bu) I/O、鏈式調用、微服務(wu)/云原生等場(chang)景(jing),預計將帶(dai)來更好的(de)延遲與(yu)(yu)吞吐表現(xian),并(bing)減少內存分配(pei)與(yu)(yu) GC 壓力。而(er)在(zai)高性(xing)(xing)能(neng)(neng)并(bing)行計算場(chang)景(jing),async/await 也(ye)能(neng)(neng)擁有(you)自己的(de)一席之地(di)。
總體(ti)而言,開(kai)發者熟悉的(de)(de) async/await 使用(yong)方式基本不變(bian);在此基礎上,Runtime Async 把同樣的(de)(de)開(kai)發體(ti)驗,推向更高的(de)(de)性(xing)能與工程效率。
