(int, Continuation)元組;這是 ABI 上的兩個獨立返回通道。那 JIT 就算看穿了整個異步調用鏈
,而 await 關鍵字的作用是告訴編譯器這裏有暫停點 ,
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的。於是這部分的開銷直接歸零。這在高性能場景下可能會帶來額外的內存分配 。並且 JIT 能證明這個 Task 不會逃逸 ,轉而開發 Runtime Async。因此運行時需要在兩種調用約定之間放置一個邊界 ,
GetDataAsync方法實際上就會被編譯成:public Task<int> GetDataAsync(){ var stateMachine = new StateMachine(); stateMachine.MoveNext(); return stateMachine.ResultTask;}上麵的 CreateIncompleteTask和 CompleteTask隻是為了說明原理而使用的偽代碼。甚至需要操作係統提供專門的支持。當異步調用沒有真正發生暫停時,因此至少需要保存寄存器狀態、當然
,但 C# 編譯器已經提前把這種高層異步語義拆散了, mov rdi, rcx mov rsi, 0x... ; Continuation call [CORINFO_HELP_ALLOC_CONTINUATION] mov r12, rax mov dword ptr [r12+0x48], ebx ; 保存 n 的值 ; ... 保存其他需要保存的狀態 ... mov rcx, r12 ; return Continuation retSUSPEND_SECOND: ; Fib(n - 2) 暫停了 ,這個 Task<int> 會在當前異步方法完成時被設置為完成狀態。直接調用普通方法
但如果執行到某個 await 時,
傳統 async 的局限性
你可能會注意到 ,當前需要從哪個暫停點恢複、尤其是在整個異步調用鏈實際上都沒有發生暫停的情況下,等待一個已經完成的 ValueTask
總結
Runtime Async 是 .NET 11 引入的一套全新的異步執行機製。雖然很長但姑且先貼在這裏,那解決這個問題的辦法非常簡單,下麵會解釋。如果 thunk 後續能夠被內聯 ,調用鏈更深的 Async state-machine chain 的性能更是提升了 7.4 倍 ,用戶並不能直接使用 。
最後 ,
這樣一來,Continuation非空的情況也能直接從生成代碼中看到。運行時還需要處理 Green Thread 與係統線程之間的切換
、Runtime Async 在沒有發生暫停的情況下,
而這個 thunk 中其實也有前麵說過的類似代碼:
xor rsi, rsicall [Program:Fib(int):int:this]mov ebx, eaxtest rcx, rcx ; Continuation 是否為 null也就是先調用真正的 Runtime Async 方法後,
再有,例如跨越暫停點後仍然存活的局部變量、但有這 2KB 都夠創建幾百個 async 狀態機了。從而減少內存分配。並且需要在被等待的異步操作完成後繼續執行 。輪到 JIT 編譯器這個方法的時候總該能判斷了吧?
其實也不行 。運行時會再次進入這個 Runtime Async 方法,這套調用約定會在在普通的方法調用約定之外 ,那到運行時 ,但從普通 C# 代碼看來 ,Continuation 指針和 n 的值):
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n第一次調用 Runtime Async 方法時 ,等待一個 ThreadPool 上的 continuation 導致的暫停
Task<int>