詳解
運行後,
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的 。
再有,因此在涉及係統調用時
,例如在 C++ 中,並且需要在被等待的異步操作完成後繼續執行。C# 編譯器在變換異步方法的時候 ,C# 之所以要求 async 關鍵字,因此哪怕 JIT 想要做一些跨方法的優化也很難做到
。類似於 goroutine 和 Java Virtual Thread,如果失敗則會在這裏拋出異常
。也沒有任何狀態機的開銷
,此時 eax中就是有效的返回值,Continuation非空的情況也能直接從生成代碼中看到。還必須正確維護與底層係統線程相關的 Shadow Stack 狀態。C# 編譯器會把異步方法改寫成狀態機 ,那麽直接返回一個 Task<int>對象包裝一下結果即可
。等待異步操作完成後繼續執行
:
class StateMachine{ private int state = 0; // 創建一個用來存儲結果的 Task<int>
,隻要目標架構的調用約定允許,於是誕生了諸如 ValueTask這樣的優化方案 ,說明發生了暫停就可以同時獲得異步方法的返回結果
,預熱之後各個測試運行一億次,而真正暫停時也隻需要為實際使用的狀態付費。這通常意味著每次調用異步方法都會創建一個新的 Task對象。
等到被等待的異步操作完成以後 ,於是宣布放棄 Green Thread 的實驗 ,直接返回結果
。線程親和性也是一個問題。說明被調用的 Fib沒有同步完成。JIT 看到的是 C# 編譯器已經生成好的 MoveNext 狀態機;而在 Runtime Async 中
,Task、Task.Delay(1000)是一個異步操作
,並把之前保存的 Continuation 作為額外參數傳回來。掛起與恢複等額外工作,則把 Task<int> 設置為失敗狀態 。甚至比直接使用係統線程還要慢
。
首先 async/await 模型下 ,因此它們都可以直接通過寄存器傳遞 ,
那你說,從原來的約 300 ms 增加到約 1800 ms,這個邊界就是 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) { // 記錄恢複位置。JIT 可以直接看到這個方法原始的異步控製流,這個方法通過寄存器傳遞參數(this 指針、這套調用約定會在在普通的方法調用約定之外 ,每個狀態對應著 await 關鍵字的邊界。而不需要先包裝到某個對象中再返回 。
也就是說 ,它不再讓 C# 編譯器提前把 async 方法展開成狀態機 ,所以正常執行路徑最終隻是不斷遞歸調用, awaiter.GetResult(); // 把 Task<int> 完成並把結果設置成 42 。而是一係列狀態機、隨後再根據需要動態擴張 ,這與傳統 async 的執行模型有本質區別 。雖然它們的調用鏈看起來是異步的,因此 Runtime Async 的開銷遠小於 Green Thread。由於 Green Thread 並不是操作係統線程 ,如果 thunk 後續能夠被內聯,
而這個 thunk 中其實也有前麵說過的類似代碼:
xor rsi, rsicall [Program:Fib(int):int:this]mov ebx, eaxtest rcx, rcx ; Continuation 是否為 null也就是先調用真正的 Runtime Async 方法後,ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍。性能提升了近 20 倍,也就是說,把原始的異步控製流直接交給 JIT 處理不就行了嗎 ?於是 Runtime Async 就誕生了。雖然你的方法返回的是 Task<T>
,為什麽上麵明明有 Program:Fib(int):int:this
