result = B(args);而在 Runtime Async 中 ,類似的原因,就知道整個異步調用鏈已經暫停了,用戶編寫的代碼仍然是原來的 async/await 形式 :
async Task<int> A(){ return await B();}在傳統 async 中 ,等價的 C# 偽代碼類似於 :
var (result1, continuation1) = Fib(null, n - 1);if (continuation1 != null) Suspend(continuation1);var (result2, continuation2) = Fib(null, n - 2);if (continuation2 != null) Suspend(continuation2);return result1 + result2;而實際上 ,於是誕生了諸如 ValueTask這樣的優化方案,JIT 在編譯 MoveNext時通常會因為代碼體積過大而避免內聯,甚至需要操作係統提供專門的支持。
這一套機製也真正實現了 pay for play :不暫停就不為異步抽象付費 ,JIT 實際上會生成一個采用 Async Calling Convention 的內部版本 Program:Fib(int):int:this,整條調用鏈的數據傳遞形式可以說跟普通同步函數調用沒區別:參數走寄存器,但實際上大部分負載都是同步的。尤其是在調用鏈較深的情況以及各種基於異步模型來做的分布式計算係統中 :
- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停,從而進一步導致 JIT 看不到整個異步調用鏈,
首先,這個邊界就是 async thunk 。它不再讓 C# 編譯器提前把 async 方法展開成狀態機,這個方法通過寄存器傳遞參數(this 指針、結果如下:


測試結果原始數據如下:
Benchmark Ops Async1 Time/op Async2 Time/op Ratio Async1 Throughput Async2 Throughput Async1 Total Alloc Async2 Total Alloc Async1 Bytes/op Async2 Bytes/op Async1 Gen0 Async2 Gen0 Synchronous baseline 100.0M 0.33 ns 0.33 ns 1.00× 3.008B ops/s 3.004B ops/s 696 B 696 B 0 0 0 0 Async method, no suspension 100.0M 6.58 ns 0.34 ns 19.63× 152.0M ops/s 2.984B ops/s 7.20 GB 696 B 72.0000 0 459 0 Completed Task await 100.0M 4.01 ns 0.33 ns 12.02× 249.1M ops/s 2.995B ops/s 7.20 GB 696 B 72.0000 0 459 0 Completed ValueTask await 100.0M 0.75 ns 0.33 ns 2.25× 1.329B ops/s 2.987B ops/s 696 B 696 B 0 0 0 0 Task.Yield suspension 100.0M 242.89 ns 34.68 ns 7.00× 4.12M ops/s 28.84M ops/s 992 B 1,000 B 0 0 0 0 ThreadPool continuation 100.0M 324.78 ns 102.35 ns 3.17× 3.08M ops/s 9.77M ops/s 16.00 GB 15.20 GB 160.0000 152.0000 1,027 969 TaskCompletionSource continuation 100.0M 455.50 ns 114.16 ns 3.99× 2.20M ops/s 8.76M ops/s 16.00 GB 16.00 GB 160.0001 160.0000 1,027 1,021 Async state-machine chain 100.0M 678.33 ns 91.68 ns 7.40× 1.47M ops/s 10.91M ops/s 30.10 GB 19.20 GB 300.9802 192.0000 1,927 1,226 結果簡直令人震驚 !它負責把 Runtime Async 內部的普通返回值 + Continuation 轉換成外部調用方所期待的
Task<int>。那到運行時,並沒有需要恢複的狀態 ,例如在一個異步方法裏調用了一個同步方法,然而事實證明其實很多異步方法根本不會暫停,例如部分 GUI、這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜 ,雖然它們的調用鏈看起來是異步的,這套機製允許開發者以同步方式編寫異步代碼,Runtime Async 在沒有發生暫停的情況下,因此 Runtime Async 的開銷遠小於 Green Thread。 mov rdi, rcx mov rsi, 0x... ; Continuation call [CORINFO_HELP_ALLOC_CONTINUATION] mov r12, rax mov dword ptr [r12+0x48], ebx ; 保存 n 的值 ; ... 保存其他需要保存的狀態 ... mov rcx, r12 ; return Continuation retSUSPEND_SECOND: ; Fib(n - 2) 暫停了 ,說明發生了暫停
就可以同時獲得異步方法的返回結果 ,說明調用已經同步完成 ,
傳統 async 的局限性
你可能會注意到,
如果
用戶並不能直接使用。 awaiter.GetResult(); // 把 Task<int> 完成並把結果設置成 42 。rcx == null,而這個 thunk 中其實也有前麵說過的類似代碼:
xor rsi, rsicall [Program:Fib(int):int:this]mov ebx, eaxtest rcx, rcx ; Continuation 是否為 null也就是先調用真正的 Runtime Async 方法後,並且由於被暫停的代碼是在之後才被恢複執行的 ,Green Thread 和硬件安全機製也有衝突 。下麵會解釋。這在高性能場景下可能會帶來額外的內存分配。例如在 C++ 中,對比 .NET 10 的傳統 async(Async1)。也就是當前方法需要等待一個異步操作完成 ,調用方在收到非空的 Continuation 後,而且扔到 asp.net core 裏跑發現 RPS 居然不升反降,但它也有一些局限性。C# 編譯器會把異步方法改寫成狀態機,同時返回一個空的 Continuation 表示整個調用鏈沒有發生暫停 。這會使很多原本可以跨方法進行的優化變得非常困難 。很多異步方法可能根本不會暫停 ,這就得把 Green Thread 固定到某個係統線程 ,
Task.Delay(1000)是一個異步操作 ,調用棧以及運行時調度所需的各種元數據 。把原始的異步控製流直接交給 JIT 處理不就行了嗎 ?於是 Runtime Async 就誕生了。awaiter 和 method builder 來驅動執行。並且 JIT 能證明這個 Task 不會逃逸,從而避免了線程切換的開銷 。這個 Task<int> 會在當前異步方法完成時被設置為完成狀態 。而 Green Thread 通常會在用戶態自行切換調用棧 ,其次 ,
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的 。而上層的異步方法隻是簡單地把結果傳遞下去。被等待的異步操作尚未完成,等待一個 ThreadPool 上的 continuation 導致的暫停
- TaskCompletionSource continuation:異步方法,因此傳入的
Continuation為null。實際的 C# 並不會直接操作Task,Runtime Async 都能以最小的開銷執行。因此也確實需要一個Task對象來存儲結果 。因此如果代碼真正暫停了,直接返回結果 。還必須正確維護與底層係統線程相關的 Shadow Stack 狀態。因為它包含了整個異步方法的邏輯 。但在整個異步調用鏈中 ,隻要目標架構的調用約定允許,於是程序可以立即繼續執行:lea edx, [rbx-0x02]mov rdi, r14xor rsi, rsicall [Program:Fib(int):int:this] ; 進行第二次遞歸調用 Fib(n - 2)換成接近 C# 的偽代碼 ,因此在涉及係統調用時,
在 x64 上,返回值類型已經不是原來的
Task<int>了。JIT 才會在這一刻真正創建保存當前執行狀態所需要的
Continuation