var result1 = Fib(n - 1);var result2 = Fib(n - 2);return result1 + result2;你會發現, // 這樣調用方在 await GetDataAsync() 時就能接收到異常並進行處理 。
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 ; 兩個調用都同步完成的情況
,等待一個嵌套了多層的異步調用鏈,實際上 , FailTask(ResultTask, ex); } }}這麽一來 ,並且需要在被等待的異步操作完成後繼續執行 。
例子
接下來讓我們看看 Runtime Async 會生成什麽樣的代碼。 // continuation 最終在哪裏執行取決於 awaiter 以及當前的 SynchronizationContext / TaskScheduler 等。.NET 的 Green Thread 實驗中發現 Green Thread 上做係統調用 1 億次
,同時返回一個空的 Continuation 表示整個調用鏈沒有發生暫停
。實際的 C# 並不會直接操作 Task,
再有,因此如果代碼真正暫停了,這套機製允許開發者以同步方式編寫異步代碼,
等到被等待的異步操作完成以後,從而引入了不必要的性能開銷。尤其是在調用鏈較深的情況以及各種基於異步模型來做的分布式計算係統中 :
- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停,等待一個已經完成的 ValueTask
- Task.Yield suspension:異步方法,
首先 ,傳統 async/await 模型每遇到一個異步方法就得進行狀態機的變換 ,調用棧以及運行時調度所需的各種元數據 。那麽當前異步調用鏈就需要暫停。這樣一來 ,為什麽上麵明明有
Program:Fib(int):int:this,用戶並不能直接使用 。對比 .NET 10 的傳統 async(Async1) 。當然這是內部表示 ,但沒有發生暫停。因此運行時不僅需要切換普通棧指針 ,而在發生暫停的情況下 ,在用戶態實現輕量級線程 ,真正的係統調用最終仍然需要由底層承載它的係統線程來執行。
傳統 async/await
.NET 自古以來就提供了 async/await 異步編程模型 ,
總結
Runtime Async 是 .NET 11 引入的一套全新的異步執行機製 。因此在涉及係統調用時,就知道整個異步調用鏈已經暫停了 ,因為它包含了整個異步方法的邏輯。Continuation 指針和 n 的值):
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n第一次調用 Runtime Async 方法時 ,
.NET 官方在實現完 Green Thread 後發現這玩意不僅局限性很大 ,
Green Thread
其實在本文即將重點介紹的 Runtime Async 之前,
Runtime Async 給 .NET 運行時引入了一套全新的調用約定 :Async Calling Convention。因此至少需要保存寄存器狀態、
最終 ,例如跨越暫停點後仍然存活的局部變量、從語義上看這些調用完全可以像普通的同步函數調用一樣執行,但在整個異步調用鏈中,JIT 實際上會生成一個采用 Async Calling Convention 的內部版本
Program:Fib(int):int:this,這時候當前Fib自己也必須暫停 。那麽這個Task<T>對象就根本不會被創建 ,Continuation非空的情況也能直接從生成代碼中看到。或者在進入相關代碼時執行額外的調度和切換 。 awaiter.OnCompleted(MoveNext); return; } goto case 1; } case 1: { state = -1; // 確認被 await 的操作已經成功完成,它不再讓 C# 編譯器提前把 async 方法展開成狀態機 ,整個異步方法就被拆分成了多個狀態機的狀態,此時運行時會保存繼續執行所需要的狀態 ,傳入的 Continuation 為 null ,Runtime Async
傳統 async/await 需要由 C# 編譯器在編譯時生成狀態機,整條調用鏈的數據傳遞形式可以說跟普通同步函數調用沒區別:參數走寄存器,
如果
rcx == null,async/await 模型下 ,於是誕生了諸如ValueTask這樣的優化方案,當代碼最終交給 JIT 時 ,也沒有任何狀態機的開銷。從而減少內存分配 。以下是一個簡單的示例:
public async Task<int> GetDataAsync(){ // 模擬異步操作 await Task.Delay(1000); return 42;}上麵這個例子中,並在被 await 的異步操作完成後繼續執行剩餘的代碼。
Task.Delay(1000)是一個異步操作,但有這 2KB 都夠創建幾百個 async 狀態機了。雖然你的方法返回的是Task<T>, mov rdi, rcx mov rsi, 0x... ; Continuation type call [CORINFO_HELP_ALLOC_CONTINUATION] mov r15, rax mov dword ptr [r15+0x4C], r12d ; 保存 Fib(n - 1) 的結果 ; ... 保存其他需要保存的狀態 ... mov rcx, r15 ; return Continuation ret; --------------------------------------------Program:Fib(int):Task<int>:this mov rdi, rbx ; this mov edx, r15d ; n xor rsi, rsi ; null Continuation call [Program:Fib(int):int:this] ; 調用真正的 Runtime Async 方法 mov ebx, eax ; result test rcx, rcx ; Continuation == null? jne THUNK_SUSPENDED ; return Task.FromResult(ebx) mov rax, <Task<int>> retTHUNK_SUSPENDED: ; var task = new RuntimeAsyncTask<int>(); ; 把 continuation 連接到 task; ; return task;可以看到對於這個方法 ,尤其是在沒有發生暫停的情況下,
MoveNext方法通常非常大 ,但從普通 C# 代碼看來,這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜,而真正暫停時也隻需要為實際使用的狀態付費 。從原來的約 300 ms 增加到約 1800 ms ,例如第一次遞歸調用 :
await Fib(n - 1)被編譯成 :
lea edx, [rbx-0x01] ; n - 1mov rdi, r14 ; thisxor rsi, rsi ; Continuation = nullcall [Program:Fib(int):int:this]而
Fib(n - 1)實際上返回了兩個值 :eax = Fib 的 int 返回值rcx = Continuation當然 ,這使得其可以在整個異步調用鏈中進行跨方法的優化,
而 await 關鍵字的作用是告訴編譯器這裏有暫停點,一旦大量代碼具有這種要求,調用鏈更深的 Async state-machine chain 的性能更是提升了 7.4 倍 ,這個邊界就是 async thunk 。
例如 , awaiter.GetResult(); // 把 Task<int> 完成並把結果設置成 42。.NET 還實驗過 Green Thread 的方案,於是
GetDataAsync方法實際上就會被編譯成 :public Task<int> GetDataAsync(){ var stateMachine = new StateMachine(); stateMachine.MoveNext(); return stateMachine.ResultTask;}上麵的
CreateIncompleteTask和CompleteTask隻是為了說明原理而使用的偽代碼。不過相信你會發現,就是 :
var (result1, continuation1) = Fib(null, n - 1);if (continuation1 != null) Suspend(continuation1);var (result2, continuation2) = Fib(null, n - 2);// ...當然 ,測試代碼見 :https://gist.github.com/hez2010/d1802e7c7ab10e21a92dcba2afe0a58d 。無論暫停還是不暫停 ,Runtime Async 都能以最小的開銷執行 。而且扔到 asp.net core 裏跑發現 RPS 居然不升反降,
還有,C# 編譯器什麽都不做,比如 GUI 應用中消息循環可能會以每秒上萬次的頻率調用線程親和的 API ,而是一係列狀態機 、
另外, state = 1; // 注冊 continuation。因為 C# 編譯器的編譯單元是方法,
- Completed Task await:異步方法,此時方法就會從上次暫停的地方繼續執行,說明調用已經同步完成,如果為 null 說明已經同步完成,於是這部分的開銷直接歸零。Runtime Async 在沒有發生暫停的情況下,它隻需要保存非常少量的東西,JIT 可以直接看到這個方法原始的異步控製流,
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的 。Task、而上層的異步方法隻是簡單地把結果傳遞下去 。它負責把 Runtime Async 內部的普通返回值 + Continuation 轉換成外部調用方所期待的
Task<int>