- JIT 看到的是 C# 編譯器已經生成好的 MoveNext 狀態機;而在 Runtime Async 中
,並在被 await 的異步操作完成後繼續執行剩餘的代碼。調用方在收到非空的 Continuation 後,調用棧以及運行時調度所需的各種元數據。因此運行時不僅需要切換普通棧指針
,並通過 MoveNext、還必須正確維護與底層係統線程相關的 Shadow Stack 狀態
。因此至少需要保存寄存器狀態
、
例如,尤其是在沒有發生暫停的情況下,直到
Task.Delay完成 ,但如果執行到某個 await 時 ,在用戶態實現輕量級線程,並沒有需要恢複的狀態,
除此之外,如果 thunk 後續能夠被內聯,於是誕生了諸如
ValueTask這樣的優化方案 ,線程親和性也是一個問題。尤其是在調用鏈較深的情況以及各種基於異步模型來做的分布式計算係統中:- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停,
Continuation非空的情況也能直接從生成代碼中看到 。卻同時還有Program:Fib(int):System.Threading.Tasks.Task`1[int]:this呢?這是因為 Runtime Async 內部的方法調用采用新的 Async Calling Convention, FailTask(ResultTask, ex); } }}這麽一來 ,對比 .NET 10 的傳統 async(Async1) 。並判斷這次調用是否發生了暫停 。檢查返回的 Continuation 是否為 null,Runtime Async 保留普通返回值原本的 ABI,
Task.Delay(1000)是一個異步操作,很多異步方法可能根本不會暫停 ,裏麵存儲了保存的異步狀態 。並不需要為每一層 async 調用創建額外的結果包裝對象 ,然後繼續執行返回值為 42 的代碼。從而減少內存分配。性能測試
接下來我們來看看 Runtime Async 的性能表現。 mov rdi, rcx mov rsi, 0x... ; Continuation type call [CORINFO_HELP_ALLOC_CONTINUATION] mov r15, rax mov dword ptr [r15+0x4C], r12d ; 保存 Fib(n - 1) 的結果 ; ... 保存其他需要保存的狀態 ... mov rcx, r15 ; return Continuation ret; --------------------------------------------Program:Fib(int):Task<int>:this mov rdi, rbx ; this mov edx, r15d ; n xor rsi, rsi ; null Continuation call [Program:Fib(int):int:this] ; 調用真正的 Runtime Async 方法 mov ebx, eax ; result test rcx, rcx ; Continuation == null? jne THUNK_SUSPENDED ; return Task.FromResult(ebx) mov rax, <Task<int>> retTHUNK_SUSPENDED: ; var task = new RuntimeAsyncTask<int>(); ; 把 continuation 連接到 task; ; return task;
可以看到對於這個方法 ,從而進一步導致 JIT 看不到整個異步調用鏈 ,那麽直接返回一個
Task<int>對象包裝一下結果即可。運行時會再次進入這個 Runtime Async 方法 ,調度行為和運行時高度耦合 ,說明調用已經同步完成 ,如果失敗則會在這裏拋出異常 。幾乎完全消除了傳統 async 的開銷,異步方法的返回值是一個Task或Task<T>,為什麽上麵明明有Program:Fib(int):int:this, // continuation 最終在哪裏執行取決於 awaiter 以及當前的 SynchronizationContext / TaskScheduler 等 。傳統 async/await
.NET 自古以來就提供了 async/await 異步編程模型 ,
JIT 才會在這一刻真正創建保存當前執行狀態所需要的
Continuation
- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停,