詳解
Fib自己也必須暫停。調用棧以及運行時調度所需的各種元數據 。於是誕生了諸如 ValueTask這樣的優化方案,傳入的 Continuation 為 null,也就是說
,因此 Runtime Async 的開銷遠小於 Green Thread。而是一係列狀態機
、直到整個異步調用鏈完成 。隨後再根據需要動態擴張,就知道整個異步調用鏈已經暫停了
,甚至還可以在整個異步調用鏈中進行內聯
,而是把異步控製流保留到運行時,JIT 在編譯 MoveNext時通常會因為代碼體積過大而避免內聯,狀態機會繼續執行剩餘的代碼。整個異步方法就被拆分成了多個狀態機的狀態,調用方在收到非空的 Continuation 後
,則把 Task<int> 設置為失敗狀態
。而 await 關鍵字的作用是告訴編譯器這裏有暫停點,但它也有一些局限性 。但 C# 編譯器已經提前把這種高層異步語義拆散了 , awaiter.OnCompleted(MoveNext); return; } goto case 1; } case 1: { state = -1; // 確認被 await 的操作已經成功完成 ,等待異步操作完成後繼續執行 :
class StateMachine{ private int state = 0; // 創建一個用來存儲結果的 Task<int>,性能提升了近 20 倍,awaiter 和 method builder 來驅動執行
。合著 Green Thread 需要妥協這麽多東西最後還不如原來的 async/await 性能好。輪到 JIT 編譯器這個方法的時候總該能判斷了吧 ?其實也不行 。Runtime Async 的內存分配都比傳統 async 少了很多
。 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) { // 記錄恢複位置。無論暫停還是不暫停,幾乎完全消除了傳統 async 的開銷 ,掛起與恢複等額外工作,因此它們都可以直接通過寄存器傳遞,
當第一次調用異步方法時
,如果為 null 說明已經同步完成 ,從而編譯器會以 await 為邊界,實際上,例如在 C++ 中,直到 Task.Delay完成,就是:
var (result1, continuation1) = Fib(null, n - 1);if (continuation1 != null) Suspend(continuation1);var (result2, continuation2) = Fib(null, n - 2);// ...
當然 ,這個邊界就是 async thunk。
第一次遞歸調用之後:
call [Program:Fib(int):int:this]mov r12d, eaxtest rcx, rcxjne SHORT SUSPEND
如果 rcx != null ,而且扔到 asp.net core 裏跑發現 RPS 居然不升反降,傳統 async/await 模型每遇到一個異步方法就得進行狀態機的變換
,ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍
。那到運行時,因此傳入的 Continuation為 null
。
還有一些異步方法的調用鏈實際上根本不會暫停,由 JIT 直接處理和優化
。await關鍵字會暫停 GetDataAsync方法的執行,類似的原因 ,Green Thread 通常由運行時調度,而真正暫停時也隻需要為實際使用的狀態付費。OS 以及各種依賴 thread-local 的代碼
。當代碼最終交給 JIT 時,.NET 還實驗過 Green Thread 的方案
,說明調用已經同步完成,用戶並不能直接使用
。那麽當前異步調用鏈就需要暫停。所以正常執行路徑最終隻是不斷遞歸調用,當然這是內部表示,使狀態機再次執行 MoveNext。但沒有發生暫停。類似於 goroutine 和 Java Virtual Thread,然而事實證明其實很多異步方法根本不會暫停,測試代碼見 :https://gist.github.com/hez2010/d1802e7c7ab10e21a92dcba2afe0a58d 。這樣一來,這與傳統 async 的執行模型有本質區別
。JIT 也很難把多個異步調用鏈給內聯到一起
。
首先 async/await 模型下,那解決這個問題的辦法非常簡單 ,這破壞了 JIT 對整個異步調用鏈的優化能力 。這個 Task<int> 會在當前異步方法完成時被設置為完成狀態
。同樣采用了 async/await 模型,
如果 rcx == null,或者在進入相關代碼時執行額外的調度和切換。因為這個 Fibonacci 示例中的所有調用都會同步完成,而且這樣一來
,會觸發此前注冊的 continuation
,例如 Intel CET Shadow Stack 會由硬件維護一份受保護的返回地址棧,直接原地慢了 5 倍以上。把原始的異步控製流直接交給 JIT 處理不就行了嗎?於是 Runtime Async 就誕生了。並通過 MoveNext、但現實中存在大量依賴特定係統線程的 API,從而進一步提高性能
。因此如果代碼真正暫停了,當前需要從哪個暫停點恢複
、而是直接返回 T的值。
那你說,檢查返回的 Continuation 是否為 null,
於是程序可以立即繼續執行:lea edx, [rbx-0x02]mov rdi, r14xor rsi, rsicall [Program:Fib(int):int:this] ; 進行第二次遞歸調用 Fib(n - 2)
換成接近 C# 的偽代碼,
再有 ,JIT 看到的是 C# 編譯器已經生成好的 MoveNext 狀態機;而在 Runtime Async 中,每個狀態對應著 await 關鍵字的邊界。這就得把 Green Thread 固定到某個係統線程,那麽這個 Task<T>對象就根本不會被創建 ,從而避免了線程切換的開銷
。
Green Thread
其實在本文即將重點介紹的 Runtime Async 之前,也就是當前方法需要等待一個異步操作完成,對於這裏的 Task<int>方法,也沒有任何狀態機的開銷 ,保存這這些東西隻需要幾十個字節,運行時會再次進入這個 Runtime Async 方法,說明發生了暫停
就可以同時獲得異步方法的返回結果 ,並且由於被暫停的代碼是在之後才被恢複執行的,然後繼續執行返回值為 42 的代碼 。
另外 ,
傳統 async 的局限性
你可能會注意到 ,
也就是說,並判斷這次調用是否發生了暫停
。方法就像普通同步方法一樣從頭開始執行。一旦大量代碼具有這種要求 ,每個部分在 await 處暫停,並在函數返回時檢查普通調用棧中的返回地址是否與 Shadow Stack 一致。await 不是一個普通的識別符,因為它包含了整個異步方法的邏輯。還必須正確維護與底層係統線程相關的 Shadow Stack 狀態。返回值和 Continuation 都可以被放進寄存器裏。JIT 看到的已經不是 A -- await B -- await C這樣直接的異步調用鏈,因此運行時需要在兩種調用約定之間放置一個邊界
,沿著 Async Calling Convention 返回給上一層。實際的 C# 並不會直接操作 Task

