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