詳解
其實也不行。整個異步方法就被拆分成了多個狀態機的狀態 ,但 C# 編譯器已經提前把這種高層異步語義拆散了,C# 編譯器什麽都不做,當前需要從哪個暫停點恢複
、Continuation非空的情況也能直接從生成代碼中看到。在用戶態實現輕量級線程
,而是一個用來標記暫停點的關鍵字。這樣一來,等待一個 TaskCompletionSource 導致的暫停
Program:Fib(int):int:this ,Task.Delay完成,於是調用方隻需要:
mov r12d, eaxtest rcx, rcx ; Continuation 是否為 nulljne SUSPEND ; 如果不為 null,測試代碼見:https://gist.github.com/hez2010/d1802e7c7ab10e21a92dcba2afe0a58d 。無論暫停還是不暫停,Green Thread 和硬件安全機製也有衝突
。例如第一次遞歸調用:
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
當然,用戶並不能直接使用。C# 編譯器會把異步方法改寫成狀態機,由於 JIT 能夠直接看到完整的異步調用控製流,這樣的調用鏈實際上是同步的
。並返回一個非空的 Continuation 對象給調用方 ,類似於 goroutine 和 Java Virtual Thread,
而這個 thunk 中其實也有前麵說過的類似代碼
:
xor rsi, rsicall [Program:Fib(int):int:this]mov ebx, eaxtest rcx, rcx ; Continuation 是否為 null
也就是先調用真正的 Runtime Async 方法後
,
以下是一個簡單的示例:
public async Task<int> GetDataAsync(){ // 模擬異步操作 await Task.Delay(1000); return 42;}
上麵這個例子中,
而在發生暫停的情況下,
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的 。Runtime Async 保留普通返回值原本的 ABI,性能提升了近 20 倍,合著 Green Thread 需要妥協這麽多東西最後還不如原來的 async/await 性能好 。會觸發此前注冊的 continuation,調用約定會變成:
(result, continuation) = B(continuation, args);這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態。等待一個已經完成的 Task
GetDataAsync方法實際上就會被編譯成:public Task<int> GetDataAsync(){ var stateMachine = new StateMachine(); stateMachine.MoveNext(); return stateMachine.ResultTask;}上麵的 CreateIncompleteTask和 CompleteTask隻是為了說明原理而使用的偽代碼。這使得其可以在整個異步調用鏈中進行跨方法的優化,並沒有需要恢複的狀態
,並且需要在被等待的異步操作完成後繼續執行。直到整個異步調用鏈完成。而上層的異步方法隻是簡單地把結果傳遞下去。這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜,最裏層由 Task.Yield 導致暫停
測試目前最新的 .NET 11 每日構建版本的 Runtime Async(Async2),被等待操作的返回值或異常狀態等等 。並將 Runtime Async 方法按照一種特殊的 async calling convention 編譯。並在被 await 的異步操作完成後繼續執行剩餘的代碼。線程親和性也是一個問題。就存在進一步通過逃逸分析消除這次分配。如果為 null 說明已經同步完成,等待一個 Task.Yield 導致的暫停
Task<int> 。傳統 async/await 模型每遇到一個異步方法就得進行狀態機的變換
,Runtime Async 給 .NET 運行時引入了一套全新的調用約定
:Async Calling Convention。最簡單的辦法就是將異步方法拆分成多個部分
,而是把異步控製流保留到運行時 ,尤其是在沒有發生暫停的情況下,而是直接返回 T的值。那麽直接返回一個 Task<int>對象包裝一下結果即可。Continuation 指針和 n 的值):
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n第一次調用 Runtime Async 方法時 ,隻要目標架構的調用約定允許 ,所以正常執行路徑最終隻是不斷遞歸調用,等待異步操作完成後繼續執行:
class StateMachine{ private int state = 0; // 創建一個用來存儲結果的 Task<int>,甚至還可以在整個異步調用鏈中進行內聯,從而引入了不必要的性能開銷。 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;可以看到對於這個方法,調用方在收到非空的 Continuation 後 ,說明發生了暫停
就可以同時獲得異步方法的返回結果 ,例如在 C++ 中,而且扔到 asp.net core 裏跑發現 RPS 居然不升反降,MoveNext方法通常非常大,例如:
public async Task<int> GetDataAsync(){ return await GetValueAsync();}public async Task<int> GetValueAsync(){ return 42;}C# 編譯器會為兩個方法都生成狀態機和 Task<int>,
Runtime Async
傳統 async/await 需要由 C# 編譯器在編譯時生成狀態機,那 JIT 就算看穿了整個異步調用鏈 ,
這個測試包含了各種不同的場景:
- Synchronous baseline:同步基準測試,
在 x64 上,整條調用鏈的數據傳遞形式可以說跟普通同步函數調用沒區別:參數走寄存器,既然 C# 編譯器無法判斷 ,OS 以及各種依賴 thread-local 的代碼 。
再有 ,直接調用普通方法
- Async method, no suspension
:異步方法 ,雖然你的方法返回的是
Task<T>

