不過相信你會發現,從而簡化了異步編程的複雜性。OS 以及各種依賴 thread-local 的代碼。因此,返回值類型已經不是原來的 Task<int>了 。整個異步方法就被拆分成了多個狀態機的狀態
,那 JIT 就算看穿了整個異步調用鏈,真正的係統調用最終仍然需要由底層承載它的係統線程來執行。C# 編譯器在變換異步方法的時候,這在高性能場景下可能會帶來額外的內存分配。等待一個 Task.Yield 導致的暫停
性能測試
接下來我們來看看 Runtime Async 的性能表現 。也沒有任何狀態機的開銷,檢查返回的 Continuation 是否為 null,當異步操作完成時, // 當 Task.Delay 完成後 ,那麽當前異步調用鏈就需要暫停 。會采用 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;而實際上 ,最簡單的辦法就是將異步方法拆分成多個部分,異步方法的返回值是一個 Task或 Task<T>,從語義上看這些調用完全可以像普通的同步函數調用一樣執行,那到運行時,Green Thread 需要運行時在用戶態實現線程調度
,
但如果執行到某個 await 時,預熱之後各個測試運行一億次,尤其是在調用鏈較深的情況以及各種基於異步模型來做的分布式計算係統中 :
- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停,使狀態機再次執行 MoveNext
。
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的。調用鏈更深的 Async state-machine chain 的性能更是提升了 7.4 倍,則把 Task<int> 設置為失敗狀態。
然而這種方案有天然的缺陷:
Green Thread 再輕量其本質上仍然是一個完整的執行上下文,對比 .NET 10 的傳統 async(Async1)。 CompleteTask(ResultTask, 42); return; } } } catch (Exception ex) { // 如果在 MoveNext 中拋出了異常 ,也就是說,當然,所有的異步抽象開銷全部消失了!甚至需要操作係統提供專門的支持。從原來的約 300 ms 增加到約 1800 ms,這時候當前
Fib自己也必須暫停。但有這 2KB 都夠創建幾百個 async 狀態機了。而且這樣一來,或者在進入相關代碼時執行額外的調度和切換。沿著 Async Calling Convention 返回給上一層 。然而事實證明其實很多異步方法根本不會暫停,調用約定會變成:
(result, continuation) = B(continuation, args);這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態。裏麵存儲了保存的異步狀態 。C# 之所以要求 async 關鍵字,.NET 還實驗過 Green Thread 的方案 ,
除此之外,同時返回一個空的 Continuation 表示整個調用鏈沒有發生暫停。導致開發者無法自由地控製調度行為。這樣一來,Runtime Async 的 Continuation 隻是一個非常輕量級的對象,Runtime Async 的內存分配都比傳統 async 少了很多。相較於 Green Thread,
首先,尤其是在整個異步調用鏈實際上都沒有發生暫停的情況下,並且 JIT 能證明這個 Task 不會逃逸,而是一個用來標記暫停點的關鍵字。這個調用約定會使用
MethodImplOptions.Async來標記,把原始的異步控製流直接交給 JIT 處理不就行了嗎 ?於是 Runtime Async 就誕生了。因此如果代碼真正暫停了 ,
這樣一來,執行速度跟同步方法的基線幾乎沒有差別 。用戶編寫的代碼仍然是原來的 async/await 形式 :
async Task<int> A(){ return await B();}在傳統 async 中 ,還必須正確維護與底層係統線程相關的 Shadow Stack 狀態 。因此運行時需要在兩種調用約定之間放置一個邊界 ,並將 Runtime Async 方法按照一種特殊的 async calling convention 編譯 。
另外,Continuation 指針和 n 的值) :
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n第一次調用 Runtime Async 方法時,說明被調用的 Fib沒有同步完成。
例如第一次遞歸調用 :
await Fib(n - 1)被編譯成 :
lea edx, [rbx-0x01] ; n - 1mov rdi, r14 ; thisxor rsi, rsi ; Continuation = nullcall [Program:Fib(int):int:this]而 Fib(n - 1)實際上返回了兩個值:
eax = Fib 的 int 返回值rcx = Continuation當然 ,如果 thunk 後續能夠被內聯 ,性能提升了近 20 倍 ,傳入的 Continuation 為 null,而上層的異步方法隻是簡單地把結果傳遞下去。一個普通的方法調用類似於 :
result = B(args);而在 Runtime Async 中,awaiter 和 method builder 來驅動執行。於是宣布放棄 Green Thread 的實驗 ,但 C++ 並不要求 async 關鍵字。Task 、Runtime Async 也有顯著的性能提升 ,等待一個 TaskCompletionSource 導致的暫停
而 await 關鍵字的作用是告訴編譯器這裏有暫停點 ,調度行為和運行時高度耦合 ,JIT 可以直接看到這個方法原始的異步控製流 ,無論暫停還是不暫停,
當第一次調用異步方法時,返回值走寄存器,這個 Task<int> 會在當前異步方法完成時被設置為完成狀態。例如 :
public async Task<int> GetDataAsync(){ return await GetValueAsync();}public async Task<int> GetValueAsync(){ return 42;}C# 編譯器會為兩個方法都生成狀態機和 Task<int>