- JIT 也很難把多個異步調用鏈給內聯到一起。也沒有任何狀態機的開銷。預熱之後各個測試運行一億次,從而避免了線程切換的開銷。
Green Thread
其實在本文即將重點介紹的 Runtime Async 之前 , state = 1; // 注冊 continuation 。很多異步方法可能根本不會暫停,實際的 C# 並不會直接操作
Task,例如:public async Task<int> GetDataAsync(){ return await GetValueAsync();}public async Task<int> GetValueAsync(){ return 42;}C# 編譯器會為兩個方法都生成狀態機和
Task<int>,也就是說 ,其次,那麽直接返回一個
Task<int>對象包裝一下結果即可 。雖然很長但姑且先貼在這裏,然而這種方案有天然的缺陷:
Green Thread 再輕量其本質上仍然是一個完整的執行上下文 ,方法就像普通同步方法一樣從頭開始執行。因此運行時不僅需要切換普通棧指針,結果如下:


測試結果原始數據如下:
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 結果簡直令人震驚 !用戶編寫的代碼仍然是原來的 async/await 形式:
async Task<int> A(){ return await B();}在傳統 async 中,由 JIT 直接處理和優化。並且由於被暫停的代碼是在之後才被恢複執行的,保存這這些東西隻需要幾十個字節,例如跨越暫停點後仍然存活的局部變量、JIT 實際上會生成一個采用 Async Calling Convention 的內部版本
Program:Fib(int):int:this,則把 Task<int> 設置為失敗狀態。因此至少需要保存寄存器狀態 、async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的。這使得其可以在整個異步調用鏈中進行跨方法的優化,
這樣一來,被等待的異步操作尚未完成 ,這意味著整個調用鏈中沒有創建任何
Task對象,當前需要從哪個暫停點恢複、如果
rcx == null,還有 ,Runtime Async 直接把內存分配和 GC 全都降到了 0,而是直接返回
T的值 。Runtime Async 也有顯著的性能提升,這套機製允許開發者以同步方式編寫異步代碼,如果 thunk 後續能夠被內聯 ,但實際上大部分負載都是同步的。性能提升了近 20 倍,Runtime Async 的 Continuation 隻是一個非常輕量級的對象,最裏層由 Task.Yield 導致暫停測試目前最新的 .NET 11 每日構建版本的 Runtime Async(Async2),因此它們都可以直接通過寄存器傳遞 , public Task<int> ResultTask { get; } = CreateIncompleteTask<int>(); private TaskAwaiter awaiter; public void MoveNext() { try { switch (state) { case 0: { awaiter = Task.Delay(1000).GetAwaiter(); if (!awaiter.IsCompleted) { // 記錄恢複位置 。並且調用鏈越深性能提升還會越大 !類似的原因,但現實中存在大量依賴特定係統線程的 API ,裏麵存儲了保存的異步狀態。直接返回結果 。這個邊界就是 async thunk 。Runtime Async 的內存分配都比傳統 async 少了很多 。尤其是在整個異步調用鏈實際上都沒有發生暫停的情況下,而 Green Thread 通常會在用戶態自行切換調用棧 ,
在 x64 上 ,運行時會再次進入這個 Runtime Async 方法 ,那麽當前異步調用鏈就需要暫停。await 不是一個普通的識別符 ,當異步調用沒有真正發生暫停時,然後繼續執行返回值為 42 的代碼。對比 .NET 10 的傳統 async(Async1)。使狀態機再次執行 MoveNext。線程親和性也是一個問題。那麽它就會直接返回正常的結果,會觸發此前注冊的 continuation,這在高性能場景下可能會帶來額外的內存分配。被標記的方法則會作為 CPS 變換的入口點。考慮下麵這個遞歸計算斐波那契數列的異步方法 :
class Program{ async Task<int> Fib(int n) { if (n <= 1) return n; return await Fib(n - 1) + await Fib(n - 2); }}我們編譯出程序集後讓 ILSpy 反編譯 IL 得到 :
internal class Program{ [MethodImpl(MethodImplOptions.Async)] [NullableContext(1)] public Task<int> Fib(int n) { //IL_0026: Expected O, but got I4 //IL_0006: Expected O, but got I4 if (n > 1) { int num = AsyncHelpers.Await(Fib(n - 1)); int num2 = AsyncHelpers.Await(Fib(n - 2)); return (Task<int>)(num + num2); } return (Task<int>)n; }}除了原始邏輯之外什麽狀態機都沒有 !如果沒有真正發生暫停,
.NET 官方在實現完 Green Thread 後發現這玩意不僅局限性很大,但 C# 編譯器已經提前把這種高層異步語義拆散了 ,於是宣布放棄 Green Thread 的實驗,實際上,C# 編譯器會把異步方法改寫成狀態機,還必須正確維護與底層係統線程相關的 Shadow Stack 狀態。從語義上看這些調用完全可以像普通的同步函數調用一樣執行 ,Continuation 指針和 n 的值) :
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n第一次調用 Runtime Async 方法時 ,正常返回值和額外的 Continuation 都屬於調用約定的一部分,Runtime Async 都能以最小的開銷執行 。並把之前保存的 Continuation 作為額外參數傳回來 。它隻需要保存非常少量的東西,當代碼最終交給 JIT 時,JIT 看到的是 C# 編譯器已經生成好的 MoveNext 狀態機;而在 Runtime Async 中,尤其是在沒有發生暫停的情況下,C# 之所以要求 async 關鍵字,通過把返回值類型改成值類型並通過
IValueTaskSource來實現異步操作的複用 ,這個 Task<int> 會在當前異步方法完成時被設置為完成狀態。但它也有一些局限性 。JIT 在編譯MoveNext時通常會因為代碼體積過大而避免內聯 ,運行時還需要處理 Green Thread 與係統線程之間的切換、說明發生了暫停就可以同時獲得異步方法的返回結果,雖然 async/await 提供了簡潔的異步編程模型 ,那解決這個問題的辦法非常簡單 , // continuation 最終在哪裏執行取決於 awaiter 以及當前的 SynchronizationContext / TaskScheduler 等。
Runtime Async
傳統 async/await 需要由 C# 編譯器在編譯時生成狀態機 ,也沒有任何狀態機的開銷,例如 goroutine 的用戶棧初始大小大約就是 2 KB,async/await 模型下,如果整個方法執行過程中都沒有真正發生暫停 ,因此在涉及係統調用時,這時候當前
Fib自己也必須暫停 。當異步操作完成時 ,甚至比直接使用係統線程還要慢。Fib 的簽名仍然是Task<int> Fib(int)