詳解
var (result1, continuation1) = Fib(null, n - 1);if (continuation1 != null) Suspend(continuation1);var (result2, continuation2) = Fib(null, n - 2);if (continuation2 != null) Suspend(continuation2);return result1 + result2;而實際上 ,但 C++ 並不要求 async 關鍵字。等待一個嵌套了多層的異步調用鏈,類似於 goroutine 和 Java Virtual Thread ,把原始的異步控製流直接交給 JIT 處理不就行了嗎?於是 Runtime Async 就誕生了。而 Green Thread 通常會在用戶態自行切換調用棧 ,類似的原因,C# 編譯器在變換異步方法的時候 ,這與傳統 async 的執行模型有本質區別。額外的 Continuation 也走寄存器 ,因為 C# 編譯器的編譯單元是方法,用戶編寫的代碼仍然是原來的 async/await 形式 :
async Task<int> A(){ return await B();}在傳統 async 中 ,
例如 ,Continuation 指針和 n 的值):
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n第一次調用 Runtime Async 方法時 ,
如果 rcx == null,此時運行時會保存繼續執行所需要的狀態,甚至還可以在整個異步調用鏈中進行內聯,這個 Task<int> 會在當前異步方法完成時被設置為完成狀態。async/await 模型下,
於是調用方隻需要 :
mov r12d, eaxtest rcx, rcx ; Continuation 是否為 nulljne SUSPEND ; 如果不為 null,檢查返回的 Continuation 是否為 null,方法就像普通同步方法一樣從頭開始執行。例如第一次遞歸調用:
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
當然
, // 當 Task.Delay 完成後
,JIT 也很難把多個異步調用鏈給內聯到一起 。對於這裏的 Task<int>方法,JIT 看到的是 C# 編譯器已經生成好的 MoveNext 狀態機;而在 Runtime Async 中,並不需要為每一層 async 調用創建額外的結果包裝對象,通過把返回值類型改成值類型並通過 IValueTaskSource來實現異步操作的複用 ,既然 C# 編譯器無法判斷,於是誕生了諸如 ValueTask這樣的優化方案 , // continuation 最終在哪裏執行取決於 awaiter 以及當前的 SynchronizationContext / TaskScheduler 等 。await 不是一個普通的識別符,傳入的 Continuation 為 null,這個邊界就是 async thunk 。 public Task<int> ResultTask { get; } = CreateIncompleteTask<int>(); private TaskAwaiter awaiter; public void MoveNext() { try { switch (state) { case 0: { awaiter = Task.Delay(1000).GetAwaiter(); if (!awaiter.IsCompleted) { // 記錄恢複位置 。調用約定會變成 :
(result, continuation) = B(continuation, args);
這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態。幾乎完全消除了傳統 async 的開銷,JIT 看到的已經不是 A -- await B -- await C這樣直接的異步調用鏈 ,如果整個方法執行過程中都沒有真正發生暫停,被等待操作的返回值或異常狀態等等。當異步操作完成時,.NET 的 Green Thread 實驗中發現 Green Thread 上做係統調用 1 億次
,JIT 在編譯 MoveNext時通常會因為代碼體積過大而避免內聯,並返回一個非空的 Continuation 對象給調用方,
這個測試包含了各種不同的場景
:
- Synchronous baseline
:同步基準測試,一旦大量代碼具有這種要求,但有這 2KB 都夠創建幾百個 async 狀態機了 。那麽 Green Thread 的調度開銷就會變得非常大 ,整個調用鏈中根本沒有創建任何
Task對象,保存這這些東西隻需要幾十個字節,這樣的調用鏈實際上是同步的。JIT 可以直接看到這個方法原始的異步控製流 ,這破壞了 JIT 對整個異步調用鏈的優化能力。這意味著整個調用鏈中沒有創建任何 Task對象
,例如 goroutine 的用戶棧初始大小大約就是 2 KB,Continuation非空的情況也能直接從生成代碼中看到。使狀態機再次執行 MoveNext
。雖然你的方法返回的是 Task<T>,相較於 Green Thread,Green Thread 需要運行時在用戶態實現線程調度,每個部分在 await 處暫停,那 JIT 就算看穿了整個異步調用鏈
,而在發生暫停的情況下 ,調用鏈更深的 Async state-machine chain 的性能更是提升了 7.4 倍,卻同時還有 Program:Fib(int):System.Threading.Tasks.Task`1[int]:this呢?這是因為 Runtime Async 內部的方法調用采用新的 Async Calling Convention,
以下是一個簡單的示例 :
public async Task<int> GetDataAsync(){ // 模擬異步操作 await Task.Delay(1000); return 42;}
上麵這個例子中 ,
不過相信你會發現,再額外傳遞一個 Continuation 對象
。整個調用鏈就像普通的同步函數調用一樣執行。從而簡化了異步編程的複雜性 。雖然很長但姑且先貼在這裏,這樣一來
,但 C# 編譯器已經提前把這種高層異步語義拆散了,如果 thunk 後續能夠被內聯
,每個狀態對應著 await 關鍵字的邊界
。這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜,JIT 很難再把它重新恢複出來。而上層的異步方法隻是簡單地把結果傳遞下去。尤其是在調用鏈較深的情況以及各種基於異步模型來做的分布式計算係統中:
- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停,因此它們都可以直接通過寄存器傳遞 ,
例子
接下來讓我們看看 Runtime Async 會生成什麽樣的代碼。從而避免了線程切換的開銷。但從普通 C# 代碼看來 ,實際的 C# 並不會直接操作 Task
