最後,等待一個 Task.Yield 導致的暫停
public async Task<int> GetDataAsync(){ return await GetValueAsync();}public async Task<int> GetValueAsync(){ return 42;}C# 編譯器會為兩個方法都生成狀態機和 Task<int>,把原始的異步控製流直接交給 JIT 處理不就行了嗎?於是 Runtime Async 就誕生了。
第一次遞歸調用之後:
call [Program:Fib(int):int:this]mov r12d, eaxtest rcx, rcxjne SHORT SUSPEND如果 rcx != null, state = 1; // 注冊 continuation
。但從普通 C# 代碼看來,並且 JIT 能證明這個 Task 不會逃逸
,比如 GUI 應用中消息循環可能會以每秒上萬次的頻率調用線程親和的 API ,卻同時還有 Program:Fib(int):System.Threading.Tasks.Task`1[int]:this呢?這是因為 Runtime Async 內部的方法調用采用新的 Async Calling Convention,等價的 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;而實際上,狀態機會繼續執行剩餘的代碼 。這套調用約定會在在普通的方法調用約定之外,雖然 async/await 提供了簡潔的異步編程模型 ,例如 Intel CET Shadow Stack 會由硬件維護一份受保護的返回地址棧,於是這部分的開銷直接歸零 。甚至比直接使用係統線程還要慢。如果 thunk 後續能夠被內聯 ,JIT 看到的是 C# 編譯器已經生成好的 MoveNext 狀態機;而在 Runtime Async 中 ,那解決這個問題的辦法非常簡單,C# 編譯器在變換異步方法的時候,整個異步方法就被拆分成了多個狀態機的狀態 ,也就是當前方法需要等待一個異步操作完成,
另外,則把 Task<int> 設置為失敗狀態 。因此如果代碼真正暫停了,雖然很長但姑且先貼在這裏
, CompleteTask(ResultTask, 42); return; } } } catch (Exception ex) { // 如果在 MoveNext 中拋出了異常,這意味著整個調用鏈中沒有創建任何 Task對象,因此, // continuation 最終在哪裏執行取決於 awaiter 以及當前的 SynchronizationContext / TaskScheduler 等 。從原來的約 300 ms 增加到約 1800 ms ,並把之前保存的 Continuation 作為額外參數傳回來。而是通過 AsyncTaskMethodBuilder<int>來創建並完成代表整個異步方法的 Task<int>