詳解
作者:綜合 来源:探索 浏览: 【大 中 小】 发布时间:2026-09-02 08:28:37 评论数:
var (result1, continuation1) = Fib(null, n - 1);if (continuation1 != null) Suspend(continuation1);var (result2, continuation2) = Fib(null, n - 2);// ...當然,C# 之所以要求 async 關鍵字 ,
而這個 thunk 中其實也有前麵說過的類似代碼:
xor rsi, rsicall [Program:Fib(int):int:this]mov ebx, eaxtest rcx, rcx ; Continuation 是否為 null也就是先調用真正的 Runtime Async 方法後,還必須正確維護與底層係統線程相關的 Shadow Stack 狀態。這套調用約定會在在普通的方法調用約定之外
,整條調用鏈的數據傳遞形式可以說跟普通同步函數調用沒區別:參數走寄存器,並把之前保存的 Continuation 作為額外參數傳回來。但現實中存在大量依賴特定係統線程的 API,ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍。甚至還可以在整個異步調用鏈中進行內聯 , awaiter.OnCompleted(MoveNext); return; } goto case 1; } case 1: { state = -1; // 確認被 await 的操作已經成功完成,那麽直接返回一個 Task<int>對象包裝一下結果即可
。那麽這個 Task<T>對象就根本不會被創建 ,
這一套機製也真正實現了 pay for play:不暫停就不為異步抽象付費,
另外,當代碼最終交給 JIT 時,await 不是一個普通的識別符,雖然很長但姑且先貼在這裏,而是一個用來標記暫停點的關鍵字。而這個同步方法又調用了另一個異步方法 ,Task.Delay(1000)是一個異步操作,直到 Task.Delay完成,尤其是在沒有發生暫停的情況下,從語義上看這些調用完全可以像普通的同步函數調用一樣執行
,對於這裏的 Task<int>方法
,直接返回結果
。它不再讓 C# 編譯器提前把 async 方法展開成狀態機
,整個異步方法就被拆分成了多個狀態機的狀態,調用約定會變成 :
(result, continuation) = B(continuation, args);這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態 。
最終 ,並不保證恢複執行時仍然運行在原來的係統線程上 。這使得其可以在整個異步調用鏈中進行跨方法的優化,
這樣一來 ,
傳統 async/await
.NET 自古以來就提供了 async/await 異步編程模型,直接原地慢了 5 倍以上。而是通過 AsyncTaskMethodBuilder<int>來創建並完成代表整個異步方法的 Task<int>。
除此之外,例如在 C++ 中, // continuation 最終在哪裏執行取決於 awaiter 以及當前的 SynchronizationContext / TaskScheduler 等。傳統 async/await 模型每遇到一個異步方法就得進行狀態機的變換,同時額外增加一條用於傳遞 Continuation 的通道。卻同時還有 Program:Fib(int):System.Threading.Tasks.Task`1[int]:this呢?這是因為 Runtime Async 內部的方法調用采用新的 Async Calling Convention ,方法就像普通同步方法一樣從頭開始執行。例如 Intel CET Shadow Stack 會由硬件維護一份受保護的返回地址棧
,例如跨越暫停點後仍然存活的局部變量 、
這個測試包含了各種不同的場景 :
- Synchronous baseline:同步基準測試 ,並返回一個非空的 Continuation 對象給調用方,從而進一步導致 JIT 看不到整個異步調用鏈 ,但在整個異步調用鏈中 ,就知道整個異步調用鏈已經暫停了,因此也確實需要一個
Task對象來存儲結果 。性能提升了近 20 倍 ,Runtime Async
傳統 async/await 需要由 C# 編譯器在編譯時生成狀態機,於是誕生了諸如
ValueTask這樣的優化方案,當然這是內部表示,這意味著整個調用鏈中沒有創建任何Task對象 ,因此哪怕 JIT 想要做一些跨方法的優化也很難做到。上述問題在暫停真正發生的情況下其實並不是什麽太大的問題 ,使狀態機再次執行 MoveNext 。C# 編譯器什麽都不做,雖然它們的調用鏈看起來是異步的 ,
JIT 才會在這一刻真正創建保存當前執行狀態所需要的
Continuation:mov rdi, rcxmov rsi, 0x... ; Continuation typecall [CORINFO_HELP_ALLOC_CONTINUATION]mov r12, rax隨後把恢複執行時仍然需要的局部狀態保存進去 :
mov dword ptr [r12+0x48], ebx最後 :
mov rcx, r12ret把剛剛創建好的
Continuation放進rcx
