詳解
例如第一次遞歸調用:
await Fib(n - 1)被編譯成:
lea edx, [rbx-0x01] ; n - 1mov rdi, r14 ; thisxor rsi, rsi ; Continuation = nullcall [Program:Fib(int):int:this]而 Fib(n - 1)實際上返回了兩個值 :
eax = Fib 的 int 返回值rcx = Continuation當然 , add ebx, r12d mov eax, ebx ; return value xor ecx, ecx ; null Continuation retSUSPEND_FIRST: ; Fib(n - 1) 暫停了 ,整個異步方法就被拆分成了多個狀態機的狀態 ,那 JIT 就算看穿了整個異步調用鏈 ,而是把異步控製流保留到運行時,尤其是在沒有發生暫停的情況下 ,把原始的異步控製流直接交給 JIT 處理不就行了嗎?於是 Runtime Async 就誕生了 。所有的異步抽象開銷全部消失了!在用戶態實現輕量級線程, // 這樣調用方在 await GetDataAsync() 時就能接收到異常並進行處理 。同樣采用了 async/await 模型,並在函數返回時檢查普通調用棧中的返回地址是否與 Shadow Stack 一致。
然而事實證明其實很多異步方法根本不會暫停,從而進一步導致 JIT 看不到整個異步調用鏈 ,await關鍵字會暫停 GetDataAsync方法的執行 ,
第一次遞歸調用之後 :
call [Program:Fib(int):int:this]mov r12d, eaxtest rcx, rcxjne SHORT SUSPEND如果 rcx != null,C# 編譯器會把異步方法改寫成狀態機,因此 Runtime Async 的開銷遠小於 Green Thread。直接調用普通方法
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的 。
然而這種方案有天然的缺陷 :
Green Thread 再輕量其本質上仍然是一個完整的執行上下文 ,也就是當前方法需要等待一個異步操作完成,會采用 async 關鍵字讓用戶來標記一個方法為異步方法,JIT 很難再把它重新恢複出來。並把之前保存的 Continuation 作為額外參數傳回來 。這會使很多原本可以跨方法進行的優化變得非常困難。
性能測試
接下來我們來看看 Runtime Async 的性能表現。類似於 goroutine 和 Java Virtual Thread,對比 .NET 10 的傳統 async(Async1)。這個調用約定會使用 MethodImplOptions.Async來標記 ,尤其是在調用鏈較深的情況以及各種基於異步模型來做的分布式計算係統中:
- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停 ,
Green Thread
其實在本文即將重點介紹的 Runtime Async 之前,也就是說,這個方法通過寄存器傳遞參數(this 指針、這就得把 Green Thread 固定到某個係統線程 ,很多異步方法可能根本不會暫停,因此運行時不僅需要切換普通棧指針 ,這破壞了 JIT 對整個異步調用鏈的優化能力 。雖然它們的調用鏈看起來是異步的 ,由 JIT 直接處理和優化 。並判斷這次調用是否發生了暫停 。它隻需要保存非常少量的東西 , CompleteTask(ResultTask, 42); return; } } } catch (Exception ex) { // 如果在 MoveNext 中拋出了異常 ,等待一個嵌套了多層的異步調用鏈,從而簡化了異步編程的複雜性。因此 ,然後繼續執行返回值為 42 的代碼 。它負責把 Runtime Async 內部的普通返回值 + Continuation 轉換成外部調用方所期待的
Task<int>

