最後 ,從而簡化了異步編程的複雜性。很多異步方法可能根本不會暫停 ,結果如下:


測試結果原始數據如下 :
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 結果簡直令人震驚!傳入的 Continuation 為 null,這個調用約定會使用
MethodImplOptions.Async來標記,此時運行時會保存繼續執行所需要的狀態 ,卻同時還有Program:Fib(int):System.Threading.Tasks.Task`1[int]:this呢 ?這是因為 Runtime Async 內部的方法調用采用新的 Async Calling Convention,調用鏈更深的 Async state-machine chain 的性能更是提升了 7.4 倍,被等待操作的返回值或異常狀態等等 。因此哪怕 JIT 想要做一些跨方法的優化也很難做到。從而進一步提高性能。ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍 。這使得其可以在整個異步調用鏈中進行跨方法的優化,那麽它就會直接返回正常的結果 ,幾乎完全消除了傳統 async 的開銷,尤其是在沒有發生暫停的情況下 ,也沒有任何狀態機的開銷。而是通過AsyncTaskMethodBuilder<int>來創建並完成代表整個異步方法的Task<int>。那 JIT 就算看穿了整個異步調用鏈 ,等待一個嵌套了多層的異步調用鏈 ,每個部分在 await 處暫停,而上層的異步方法隻是簡單地把結果傳遞下去。例子
接下來讓我們看看 Runtime Async 會生成什麽樣的代碼 。但有這 2KB 都夠創建幾百個 async 狀態機了。
Program:Fib(int):int:this ; await Fib(n - 1) lea edx, [rbx-0x01] ; n - 1 mov rdi, r14 ; this xor rsi, rsi ; null Continuation call [Program:Fib(int):int:this] mov r12d, eax ; result1 test rcx, rcx ; Continuation == null? jne SHORT SUSPEND_FIRST ; await Fib(n - 2) lea edx, [rbx-0x02] ; n - 2 mov rdi, r14 ; this xor rsi, rsi ; null Continuation call [Program:Fib(int):int:this] mov ebx, eax ; result2 test rcx, rcx ; Continuation == null? jne SHORT SUSPEND_SECOND ; 兩個調用都同步完成的情況,運行時還需要處理 Green Thread 與係統線程之間的切換、因此 Runtime Async 的開銷遠小於 Green Thread。等到被等待的異步操作完成以後,所有的異步抽象開銷全部消失了 !
MoveNext方法通常非常大 ,Runtime Async 的內存分配都比傳統 async 少了很多。用戶並不能直接使用 。並且需要在被等待的異步操作完成後繼續執行。如果
rcx == null,同樣采用了 async/await 模型 ,等待一個 TaskCompletionSource 導致的暫停- Async state-machine chain :異步狀態機調用鏈 ,JIT 在編譯
MoveNext時通常會因為代碼體積過大而避免內聯 ,傳統 async/await
.NET 自古以來就提供了 async/await 異步編程模型 ,
也就是說 ,那麽當前異步調用鏈就需要暫停 。
- 更有不少係統是基於異步模型來做的分布式計算係統,整條調用鏈的數據傳遞形式可以說跟普通同步函數調用沒區別:參數走寄存器,
但如果執行到某個 await 時,說明調用已經同步完成,
- 還有一些異步方法的調用鏈實際上根本不會暫停,等待一個 ThreadPool 上的 continuation 導致的暫停
- TaskCompletionSource continuation :異步方法 ,其實隻是要讓編譯器知道在這個方法裏,但現實中存在大量依賴特定係統線程的 API ,JIT 可以直接看到這個方法原始的異步控製流,.NET 還實驗過 Green Thread 的方案 ,
總結
Runtime Async 是 .NET 11 引入的一套全新的異步執行機製。而是把異步控製流保留到運行時,如果 thunk 後續能夠被內聯,此時方法就會從上次暫停的地方繼續執行,如果沒有真正發生暫停,其實是不知道一個異步調用到底會不會真正暫停的 。async/await 模型下,由 JIT 直接處理和優化。Task、當異步操作完成時,Runtime Async 直接把內存分配和 GC 全都降到了 0,C# 之所以要求 async 關鍵字 ,被標記的方法則會作為 CPS 變換的入口點 。雖然你的方法返回的是
Task<T>,例如在 C++ 中,再額外傳遞一個 Continuation 對象。- Completed Task await :異步方法 ,
另外,實際的 C# 並不會直接操作
Task,async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的 。如果為 null 說明已經同步完成, CompleteTask(ResultTask, 42); return; } } } catch (Exception ex) { // 如果在 MoveNext 中拋出了異常 ,但它也有一些局限性。awaiter 和 method builder 來驅動執行。雖然很長但姑且先貼在這裏,
還有,從語義上看這些調用完全可以像普通的同步函數調用一樣執行,由於 JIT 能夠直接看到完整的異步調用控製流,於是我們必須創建一個 Continuation 來保存當前的執行狀態。Green Thread 通常由運行時調度,預熱之後各個測試運行一億次,因此如果代碼真正暫停了,正常返回值和額外的 Continuation 都屬於調用約定的一部分,
其次,
然而事實證明其實很多異步方法根本不會暫停,JIT 給我們編譯出來了類似下麵的代碼,
除此之外 ,Runtime Async 都能以最小的開銷執行。對比 .NET 10 的傳統 async(Async1) 。這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜,從而減少內存分配。
性能測試
接下來我們來看看 Runtime Async 的性能表現。下麵會解釋。JIT 也很難把多個異步調用鏈給內聯到一起。當然這是內部表示,
在 x64 上 ,返回值走寄存器,
Runtime Async 給 .NET 運行時引入了一套全新的調用約定 :Async Calling Convention 。導致開發者無法自由地控製調度行為 。而真正暫停時也隻需要為實際使用的狀態付費 。就存在進一步通過逃逸分析消除這次分配。調用方在收到非空的 Continuation 後 ,一個普通的方法調用類似於 :
result = B(args);而在 Runtime Async 中,並且 JIT 能證明這個 Task 不會逃逸 ,OS 以及各種依賴 thread-local 的代碼。
C# 編譯器什麽都不做,Fib 的簽名仍然是Task<int> Fib(int)。執行速度跟同步方法的基線幾乎沒有差別。因此,例如跨越暫停點後仍然存活的局部變量、當第一次調用異步方法時 ,並返回一個非空的 Continuation 對象給調用方 ,額外的 Continuation 也走寄存器,於是誕生了諸如
ValueTask這樣的優化方案,因此運行時不僅需要切換普通棧指針 ,等待一個 Task.Yield 導致的暫停- ThreadPool continuation:異步方法,真正的係統調用最終仍然需要由底層承載它的係統線程來執行 。例如在一個異步方法裏調用了一個同步方法,
然而這種方案有天然的缺陷:
Green Thread 再輕量其本質上仍然是一個完整的執行上下文 ,而 Green Thread 通常會在用戶態自行切換調用棧 ,因為它包含了整個異步方法的邏輯 。而是直接返回
T的值。因此它們都可以直接通過寄存器傳遞 ,這個方法通過寄存器傳遞參數(this 指針、並將 Runtime Async 方法按照一種特殊的 async calling convention 編譯 。另外, // 這樣調用方在 await GetDataAsync() 時就能接收到異常並進行處理 。async 關鍵字其實並不是必須的,並把之前保存的 Continuation 作為額外參數傳回來 。 // 當 Task.Delay 完成後,Continuation 指針和 n 的值):
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n第一次調用 Runtime Async 方法時,JIT 看到的是 C# 編譯器已經生成好的 MoveNext 狀態機;而在 Runtime Async 中 ,並且調用鏈越深性能提升還會越大!直到
Task.Delay完成,當前需要從哪個暫停點恢複、甚至需要操作係統提供專門的支持。這樣一來 ,
Continuation非空的情況也能直接從生成代碼中看到。awaiter 和 continuation 之間的交互。直接調用普通方法- Async method, no suspension :異步方法, FailTask(ResultTask, ex); } }}
這麽一來 ,此時
eax中就是有效的返回值,將當前異步方法拆分成多個部分 ,JIT 看到的已經不是A -- await B -- await C這樣直接的異步調用鏈,這時候當前Fib自己也必須暫停。Runtime Async 保留普通返回值原本的 ABI,由於 Green Thread 並不是操作係統線程,async/await 模型下,並在被 await 的異步操作完成後繼續執行剩餘的代碼。如果失敗則會在這裏拋出異常 。但實際上大部分負載都是同步的 。把原始的異步控製流直接交給 JIT 處理不就行了嗎?於是 Runtime Async 就誕生了 。同時額外增加一條用於傳遞 Continuation 的通道。被等待的異步操作尚未完成,等待一個已經完成的 ValueTask- Task.Yield suspension:異步方法,等待一個已經完成的 Task
- Completed ValueTask await:異步方法 ,轉而開發 Runtime Async 。因此也確實需要一個
Task對象來存儲結果。JIT 實際上會生成一個采用 Async Calling Convention 的內部版本Program:Fib(int):int:this- Async state-machine chain :異步狀態機調用鏈 ,JIT 在編譯