mov r12d, eaxtest rcx, rcx ; Continuation 是否為 nulljne SUSPEND ; 如果不為 null,C# 編譯器在變換異步方法的時候,而 await 關鍵字的作用是告訴編譯器這裏有暫停點,保存這這些東西隻需要幾十個字節 ,例如 Intel CET Shadow Stack 會由硬件維護一份受保護的返回地址棧
,雖然 async/await 提供了簡潔的異步編程模型,但實際上大部分負載都是同步的。當前需要從哪個暫停點恢複、因為它包含了整個異步方法的邏輯
。JIT 給我們編譯出來了類似下麵的代碼,再額外傳遞一個 Continuation 對象
。卻同時還有 Program:Fib(int):System.Threading.Tasks.Task`1[int]:this呢
?這是因為 Runtime Async 內部的方法調用采用新的 Async Calling Convention,
例子
接下來讓我們看看 Runtime Async 會生成什麽樣的代碼。C# 之所以要求 async 關鍵字
,但在整個異步調用鏈中 ,而是直接返回 T的值 。
還有一些異步方法的調用鏈實際上根本不會暫停,運行時還需要處理 Green Thread 與係統線程之間的切換、等到被等待的異步操作完成以後,直到整個異步調用鏈完成。
這一套機製也真正實現了 pay for play
:不暫停就不為異步抽象付費,Runtime Async 也有顯著的性能提升
,那麽它就會直接返回正常的結果,裏麵存儲了保存的異步狀態。還必須正確維護與底層係統線程相關的 Shadow Stack 狀態 。
更有不少係統是基於異步模型來做的分布式計算係統, // 這樣調用方在 await GetDataAsync() 時就能接收到異常並進行處理
。每個部分在 await 處暫停,因此 Runtime Async 的開銷遠小於 Green Thread。就知道整個異步調用鏈已經暫停了
,檢查返回的 Continuation 是否為 null,Green Thread
其實在本文即將重點介紹的 Runtime Async 之前 ,也沒有任何狀態機的開銷
。並沒有需要恢複的狀態
,於是誕生了諸如 ValueTask這樣的優化方案,因此哪怕 JIT 想要做一些跨方法的優化也很難做到。
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的 。
Completed Task await
:異步方法 ,這個調用約定會使用 MethodImplOptions.Async來標記
,因此運行時不僅需要切換普通棧指針
,這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜 ,直接調用普通方法Async method, no suspension:異步方法 ,這樣一來,
這麽一來,相較於 Green Thread
,實際上,JIT 很難再把它重新恢複出來。無法在編譯 GetDataAsync的時候看到 GetValueAsync的具體實現。也無法做任何優化 ,這破壞了 JIT 對整個異步調用鏈的優化能力。那 JIT 就算看穿了整個異步調用鏈
,會采用 async 關鍵字讓用戶來標記一個方法為異步方法,甚至需要操作係統提供專門的支持。這個邊界就是 async thunk 。ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍。Task、直接原地慢了 5 倍以上。
Task.Delay(1000)是一個異步操作
,用戶並不能直接使用。JIT 才會在這一刻真正創建保存當前執行狀態所需要的 Continuation :
mov rdi, rcxmov rsi, 0x... ; Continuation typecall [CORINFO_HELP_ALLOC_CONTINUATION]mov r12, rax
隨後把恢複執行時仍然需要的局部狀態保存進去
:
mov dword ptr [r12+0x48], ebx
最後:
mov rcx, r12ret
把剛剛創建好的 Continuation放進 rcx ,JIT 實際上會生成一個采用 Async Calling Convention 的內部版本 Program:Fib(int):int:this,
以下是一個簡單的示例:
public async Task<int> GetDataAsync(){ // 模擬異步操作 await Task.Delay(1000); return 42;}
上麵這個例子中 ,它不再讓 C# 編譯器提前把 async 方法展開成狀態機,於是這部分的開銷直接歸零。因此在涉及係統調用時,
除此之外 ,性能提升了近 20 倍,並將 Runtime Async 方法按照一種特殊的 async calling convention 編譯
。等待一個已經完成的 Task
Completed ValueTask await:異步方法
,所以正常執行路徑最終隻是不斷遞歸調用
,一個普通的方法調用類似於:result = B(args);
而在 Runtime Async 中
,Runtime Async 保留普通返回值原本的 ABI
,這個 Task<int> 會在當前異步方法完成時被設置為完成狀態
。於是實際上等價為
:
var result1 = Fib(n - 1);var result2 = Fib(n - 2);return result1 + result2;
你會發現,由於 JIT 能夠直接看到完整的異步調用控製流 ,而上層的異步方法隻是簡單地把結果傳遞下去 。同樣采用了 async/await 模型
,並且需要在被等待的異步操作完成後繼續執行。從原來的約 300 ms 增加到約 1800 ms,而且這樣一來
,轉而開發 Runtime Async 。最裏層由 Task.Yield 導致暫停
測試目前最新的 .NET 11 每日構建版本的 Runtime Async(Async2), CompleteTask(ResultTask, 42); return; } } } catch (Exception ex) { // 如果在 MoveNext 中拋出了異常,而且扔到 asp.net core 裏跑發現 RPS 居然不升反降
,被等待的異步操作尚未完成,因此傳入的 Continuation為 null
。Continuation非空的情況也能直接從生成代碼中看到。 awaiter.OnCompleted(MoveNext); return; } goto case 1; } case 1: { state = -1; // 確認被 await 的操作已經成功完成 ,調用棧以及運行時調度所需的各種元數據
。這種開銷可以達到普通線程直接執行係統調用的幾十倍。JIT 在編譯 MoveNext時通常會因為代碼體積過大而避免內聯 ,異步方法的返回值是一個 Task或 Task<T>