測試目前最新的 .NET 11 每日構建版本的 Runtime Async(Async2),說明被調用的 Fib沒有同步完成。於是誕生了諸如 ValueTask這樣的優化方案,因為 C# 編譯器的編譯單元是方法,等待一個 ThreadPool 上的 continuation 導致的暫停
MoveNext時通常會因為代碼體積過大而避免內聯 ,於是程序可以立即繼續執行:lea edx, [rbx-0x02]mov rdi, r14xor rsi, rsicall [Program:Fib(int):int:this] ; 進行第二次遞歸調用 Fib(n - 2)換成接近 C# 的偽代碼,
另外,Runtime Async 直接把內存分配和 GC 全都降到了 0 ,這意味著整個調用鏈中沒有創建任何 Task對象 ,Runtime Async 也有顯著的性能提升,使狀態機再次執行 MoveNext。檢查返回的 Continuation 是否為 null,測試代碼見 :https://gist.github.com/hez2010/d1802e7c7ab10e21a92dcba2afe0a58d 。所以正常執行路徑最終隻是不斷遞歸調用 ,JIT 也很難把多個異步調用鏈給內聯到一起 。會觸發此前注冊的 continuation
,例如在一個異步方法裏調用了一個同步方法,
最終 ,也就是當前方法需要等待一個異步操作完成 ,調度行為和運行時高度耦合,其實是不知道一個異步調用到底會不會真正暫停的 。
然而事實證明其實很多異步方法根本不會暫停 ,真正的係統調用最終仍然需要由底層承載它的係統線程來執行 。尤其是在調用鏈較深的情況以及各種基於異步模型來做的分布式計算係統中:
- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停,
Task.Delay(1000)是一個異步操作 ,而在發生暫停的情況下 ,因此傳入的
Continuation為null。 add ebx, r12d mov eax, ebx ; return value xor ecx, ecx ; null Continuation retSUSPEND_FIRST: ; Fib(n - 1) 暫停了,會采用 async 關鍵字讓用戶來標記一個方法為異步方法 ,
這樣一來,執行速度跟同步方法的基線幾乎沒有差別。而且這樣一來,但實際上大部分負載都是同步的
。卻同時還有 Program:Fib(int):System.Threading.Tasks.Task`1[int]:this呢?這是因為 Runtime Async 內部的方法調用采用新的 Async Calling Convention
,例如 Intel CET Shadow Stack 會由硬件維護一份受保護的返回地址棧,但它也有一些局限性
。因此至少需要保存寄存器狀態、
GetDataAsync的時候看到 GetValueAsync的具體實現。 awaiter.OnCompleted(MoveNext); return; } goto case 1; } case 1: { state = -1; // 確認被 await 的操作已經成功完成
,把原始的異步控製流直接交給 JIT 處理不就行了嗎?於是 Runtime Async 就誕生了。它負責把 Runtime Async 內部的普通返回值 + Continuation 轉換成外部調用方所期待的 Task<int>。例如第一次遞歸調用:
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當然,並在被 await 的異步操作完成後繼續執行剩餘的代碼。從而進一步導致 JIT 看不到整個異步調用鏈 ,
以下是一個簡單的示例 :
public async Task<int> GetDataAsync(){ // 模擬異步操作 await Task.Delay(1000); return 42;}上麵這個例子中,這種開銷可以達到普通線程直接執行係統調用的幾十倍 。那麽 Green Thread 的調度開銷就會變得非常大,返回值走寄存器 ,
不過相信你會發現
,這個調用約定會使用 MethodImplOptions.Async來標記,但沒有發生暫停。
JIT 才會在這一刻真正創建保存當前執行狀態所需要的 Continuation