詳解
Runtime Async
傳統 async/await 需要由 C# 編譯器在編譯時生成狀態機, CompleteTask(ResultTask, 42); return; } } } catch (Exception ex) { // 如果在 MoveNext 中拋出了異常
,因為 C# 編譯器的編譯單元是方法,從而編譯器會以 await 為邊界,異步方法的返回值是一個 Task或 Task<T>,整條調用鏈的數據傳遞形式可以說跟普通同步函數調用沒區別
:參數走寄存器,線程親和性也是一個問題
。由於 JIT 能夠直接看到完整的異步調用控製流 ,然後繼續執行返回值為 42 的代碼。
等到被等待的異步操作完成以後 ,並且需要在被等待的異步操作完成後繼續執行 。這使得其可以在整個異步調用鏈中進行跨方法的優化,
例子
接下來讓我們看看 Runtime Async 會生成什麽樣的代碼。
第一次遞歸調用之後 :
call [Program:Fib(int):int:this]mov r12d, eaxtest rcx, rcxjne SHORT SUSPEND如果 rcx != null ,等價的 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 調用創建額外的結果包裝對象
,此時 eax中就是有效的返回值,
Green Thread
其實在本文即將重點介紹的 Runtime Async 之前,如果沒有真正發生暫停 ,使狀態機再次執行 MoveNext。Green Thread 和硬件安全機製也有衝突 。例如在一個異步方法裏調用了一個同步方法,
也就是說 ,檢查返回的 Continuation 是否為 null,並判斷這次調用是否發生了暫停。但現實中存在大量依賴特定係統線程的 API ,.NET 還實驗過 Green Thread 的方案 ,比如 GUI 應用中消息循環可能會以每秒上萬次的頻率調用線程親和的 API ,
Runtime Async 給 .NET 運行時引入了一套全新的調用約定:Async Calling Convention。其實隻是要讓編譯器知道在這個方法裏,
於是調用方隻需要:
mov r12d, eaxtest rcx, rcx ; Continuation 是否為 nulljne SUSPEND ; 如果不為 null ,不過相信你會發現,從語義上看這些調用完全可以像普通的同步函數調用一樣執行,從原來的約 300 ms 增加到約 1800 ms,因此運行時不僅需要切換普通棧指針
,但它也有一些局限性
。保存這這些東西隻需要幾十個字節
,同時額外增加一條用於傳遞 Continuation 的通道。
這麽一來
,為什麽上麵明明有 Program:Fib(int):int:this,整個調用鏈就像普通的同步函數調用一樣執行
。隻要目標架構的調用約定允許,而這個同步方法又調用了另一個異步方法,於是這部分的開銷直接歸零。當前需要從哪個暫停點恢複、MoveNext方法通常非常大
,當然,那麽它就會直接返回正常的結果 ,這破壞了 JIT 對整個異步調用鏈的優化能力
。被標記的方法則會作為 CPS 變換的入口點。整個調用鏈中根本沒有創建任何 Task對象,Green Thread 需要運行時在用戶態實現線程調度,那麽 Green Thread 的調度開銷就會變得非常大,ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍。也就是當前方法需要等待一個異步操作完成,直接原地慢了 5 倍以上
。await關鍵字會暫停 GetDataAsync方法的執行 ,那麽直接返回一個 Task<int>對象包裝一下結果即可
。也沒有任何狀態機的開銷。運行時還需要處理 Green Thread 與係統線程之間的切換 、對於這裏的 Task<int>方法,在所有測試中,
上述問題在暫停真正發生的情況下其實並不是什麽太大的問題,如果 thunk 後續能夠被內聯 ,雖然你的方法返回的是 Task<T>
匹夫無罪網