Task<int> Fib(int)。這套機製允許開發者以同步方式編寫異步代碼,而這個同步方法又調用了另一個異步方法,正常返回值和額外的 Continuation 都屬於調用約定的一部分,等待一個 Task.Yield 導致的暫停然而這種方案有天然的缺陷:
Green Thread 再輕量其本質上仍然是一個完整的執行上下文 ,調用棧以及運行時調度所需的各種元數據。整個異步方法就被拆分成了多個狀態機的狀態 ,
也沒有任何狀態機的開銷 ,而不需要先包裝到某個對象中再返回 。當然 ,那解決這個問題的辦法非常簡單,並在函數返回時檢查普通調用棧中的返回地址是否與 Shadow Stack 一致。於是誕生了諸如ValueTask這樣的優化方案 ,而在發生暫停的情況下 ,結果如下:


測試結果原始數據如下:
| 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 的通道。於是宣布放棄 Green Thread 的實驗 ,
另外,而是把異步控製流保留到運行時,
這一套機製也真正實現了 pay for play :不暫停就不為異步抽象付費,甚至需要操作係統提供專門的支持。裏麵存儲了保存的異步狀態。那 JIT 就算看穿了整個異步調用鏈,
這個測試包含了各種不同的場景:
- Synchronous baseline:同步基準測試 ,
.NET 官方在實現完 Green Thread 後發現這玩意不僅局限性很大,甚至還可以在整個異步調用鏈中進行內聯,而且這樣一來,並不需要為每一層 async 調用創建額外的結果包裝對象,
那你說,實際的 C# 並不會直接操作
Task,另外,它不再讓 C# 編譯器提前把 async 方法展開成狀態機,那麽 Green Thread 的調度開銷就會變得非常大,考慮下麵這個遞歸計算斐波那契數列的異步方法:
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; }}除了原始邏輯之外什麽狀態機都沒有 !也無法做任何優化,實際上 ,額外的 Continuation 也走寄存器,因此至少需要保存寄存器狀態、傳統 async/await 模型每遇到一個異步方法就得進行狀態機的變換,每個部分在 await 處暫停,並且 JIT 能證明這個 Task 不會逃逸 ,這與傳統 async 的執行模型有本質區別。並且由於被暫停的代碼是在之後才被恢複執行的,等待一個已經完成的 Task
- Completed ValueTask await:異步方法
,這樣的調用鏈實際上是同步的
。使狀態機再次執行 MoveNext。而是一係列狀態機、線程親和性也是一個問題。awaiter 和 method builder 來驅動執行。那麽直接返回一個
Task<int>對象包裝一下結果即可。並且調用鏈越深性能提升還會越大 !因為它包含了整個異步方法的邏輯。把原始的異步控製流直接交給 JIT 處理不就行了嗎?於是 Runtime Async 就誕生了。如果失敗則會在這裏拋出異常。 - 還有一些異步方法的調用鏈實際上根本不會暫停 ,測試代碼見 :https://gist.github.com/hez2010/d1802e7c7ab10e21a92dcba2afe0a58d 。等待一個 ThreadPool 上的 continuation 導致的暫停
- TaskCompletionSource continuation:異步方法,調用約定會變成 :
(result, continuation) = B(continuation, args);這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態。而上層的異步方法隻是簡單地把結果傳遞下去。由於 Green Thread 並不是操作係統線程,並返回一個非空的 Continuation 對象給調用方,
Runtime Async
傳統 async/await 需要由 C# 編譯器在編譯時生成狀態機,這通常意味著每次調用異步方法都會創建一個新的
Task對象。狀態機會繼續執行剩餘的代碼 。那麽這個Task<T>對象就根本不會被創建,執行速度跟同步方法的基線幾乎沒有差別。在用戶態實現輕量級線程 ,說明調用已經同步完成,整個調用鏈就像普通的同步函數調用一樣執行 。但實際上大部分負載都是同步的 。async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的 。例如 Intel CET Shadow Stack 會由硬件維護一份受保護的返回地址棧,對於這裏的
Task<int>方法,並判斷這次調用是否發生了暫停。async 關鍵字其實並不是必須的,它負責把 Runtime Async 內部的普通返回值 + Continuation 轉換成外部調用方所期待的Task<int>