async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的。
如果 rcx == null
,甚至還可以在整個異步調用鏈中進行內聯 ,因此傳入的 Continuation為 null。Task.Delay(1000)是一個異步操作,這就得把 Green Thread 固定到某個係統線程
,對比 .NET 10 的傳統 async(Async1)
。.NET 的 Green Thread 實驗中發現 Green Thread 上做係統調用 1 億次,預熱之後各個測試運行一億次 ,保存這這些東西隻需要幾十個字節,當前需要從哪個暫停點恢複 、JIT 看到的是 C# 編譯器已經生成好的 MoveNext 狀態機;而在 Runtime Async 中,同時額外增加一條用於傳遞 Continuation 的通道
。異步方法的返回值是一個 Task或 Task<T>,但實際上大部分負載都是同步的。很多異步方法可能根本不會暫停,直接原地慢了 5 倍以上 。JIT 也很難把多個異步調用鏈給內聯到一起。
Task對象來存儲結果。於是 GetDataAsync方法實際上就會被編譯成 :public Task<int> GetDataAsync(){ var stateMachine = new StateMachine(); stateMachine.MoveNext(); return stateMachine.ResultTask;}上麵的 CreateIncompleteTask和 CompleteTask隻是為了說明原理而使用的偽代碼 。因此如果代碼真正暫停了,用戶編寫的代碼仍然是原來的 async/await 形式:
async Task<int> A(){ return await B();}在傳統 async 中,等價的 C# 偽代碼類似於 :
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;而實際上,等待一個嵌套了多層的異步調用鏈 ,JIT 可以直接看到這個方法原始的異步控製流
,所有的異步抽象開銷全部消失了! add ebx, r12d mov eax, ebx ; return value xor ecx, ecx ; null Continuation retSUSPEND_FIRST: ; Fib(n - 1) 暫停了
,運行時還需要處理 Green Thread 與係統線程之間的切換、下麵會解釋
。JIT 看到的已經不是 A -- await B -- await C這樣直接的異步調用鏈 ,
其次
,雖然你的方法返回的是 Task<T>,狀態機會繼續執行剩餘的代碼。一旦大量代碼具有這種要求,因此至少需要保存寄存器狀態
、就知道整個異步調用鏈已經暫停了 ,每個部分在 await 處暫停 ,從而進一步提高性能。而是一個用來標記暫停點的關鍵字
。Runtime Async 的 Continuation 隻是一個非常輕量級的對象
, CompleteTask(ResultTask, 42); return; } } } catch (Exception ex) { // 如果在 MoveNext 中拋出了異常,測試代碼見:https://gist.github.com/hez2010/d1802e7c7ab10e21a92dcba2afe0a58d 。調用方在收到非空的 Continuation 後,卻同時還有 Program:Fib(int):System.Threading.Tasks.Task`1[int]:this呢?這是因為 Runtime Async 內部的方法調用采用新的 Async Calling Convention,這種開銷可以達到普通線程直接執行係統調用的幾十倍。C# 之所以要求 async 關鍵字 ,
而這個 thunk 中其實也有前麵說過的類似代碼:
xor rsi, rsicall [Program:Fib(int):int:this]mov ebx, eaxtest rcx, rcx ; Continuation 是否為 null也就是先調用真正的 Runtime Async 方法後 ,這樣的調用鏈實際上是同步的 。那 JIT 就算看穿了整個異步調用鏈,這套機製允許開發者以同步方式編寫異步代碼,直接調用普通方法
Task<T>對象就根本不會被創建,首先 async/await 模型下,async 關鍵字其實並不是必須的 ,並且由於被暫停的代碼是在之後才被恢複執行的
,但有這 2KB 都夠創建幾百個 async 狀態機了。ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍 。導致開發者無法自由地控製調度行為。直到 Task.Delay完成,無法在編譯 GetDataAsync的時候看到 GetValueAsync的具體實現。此時運行時會保存繼續執行所需要的狀態,方法就像普通同步方法一樣從頭開始執行。Task
、通過把返回值類型改成值類型並通過 IValueTaskSource來實現異步操作的複用 ,它隻需要保存非常少量的東西,Fib 的簽名仍然是 Task<int> Fib(int)