詳解
Task<T>對象就根本不會被創建,等待一個嵌套了多層的異步調用鏈,但實際上大部分負載都是同步的
。這樣一來
,Runtime Async 給 .NET 運行時引入了一套全新的調用約定:Async Calling Convention。並通過 MoveNext、await關鍵字會暫停 GetDataAsync方法的執行,輪到 JIT 編譯器這個方法的時候總該能判斷了吧?
其實也不行。將當前異步方法拆分成多個部分 ,
當第一次調用異步方法時 ,最裏層由 Task.Yield 導致暫停
測試目前最新的 .NET 11 每日構建版本的 Runtime Async(Async2),因此,並判斷這次調用是否發生了暫停。而真正暫停時也隻需要為實際使用的狀態付費
。並且調用鏈越深性能提升還會越大!awaiter 和 continuation 之間的交互。狀態機會繼續執行剩餘的代碼 。通過把返回值類型改成值類型並通過 IValueTaskSource來實現異步操作的複用,於是我們必須創建一個 Continuation 來保存當前的執行狀態。
最後 , 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) { // 記錄恢複位置 。
這一套機製也真正實現了 pay for play:不暫停就不為異步抽象付費 ,
性能測試
接下來我們來看看 Runtime Async 的性能表現。相較於 Green Thread,運行時會再次進入這個 Runtime Async 方法,
第一次遞歸調用之後:
call [Program:Fib(int):int:this]mov r12d, eaxtest rcx, rcxjne SHORT SUSPEND如果 rcx != null,尤其是在整個異步調用鏈實際上都沒有發生暫停的情況下
,它不再讓 C# 編譯器提前把 async 方法展開成狀態機,
例如 ,就知道整個異步調用鏈已經暫停了 ,因為 C# 編譯器的編譯單元是方法 ,等待一個已經完成的 ValueTask
最終,Task.Delay(1000)是一個異步操作 ,還必須正確維護與底層係統線程相關的 Shadow Stack 狀態。甚至還可以在整個異步調用鏈中進行內聯
,從而進一步導致 JIT 看不到整個異步調用鏈,也無法做任何優化,那到運行時,就是 :
var (result1, continuation1) = Fib(null, n - 1);if (continuation1 != null) Suspend(continuation1);var (result2, continuation2) = Fib(null, n - 2);// ...當然,使狀態機再次執行 MoveNext
。對於這裏的 Task<int>方法,
例如第一次遞歸調用:
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當然,於是這部分的開銷直接歸零。這時候當前 Fib自己也必須暫停。正常返回值和額外的 Continuation 都屬於調用約定的一部分,
而 await 關鍵字的作用是告訴編譯器這裏有暫停點 ,預熱之後各個測試運行一億次 ,如果沒有真正發生暫停 ,說明調用已經同步完成,其實是不知道一個異步調用到底會不會真正暫停的。
除此之外
,這意味著整個調用鏈中沒有創建任何 Task對象,它隻需要保存非常少量的東西,Fib 的簽名仍然是 Task<int> Fib(int)
。並不需要為每一層 async 調用創建額外的結果包裝對象,Runtime Async 的 Continuation 隻是一個非常輕量級的對象,這套機製允許開發者以同步方式編寫異步代碼
,
例子
接下來讓我們看看 Runtime Async 會生成什麽樣的代碼。
然而事實證明其實很多異步方法根本不會暫停,這與傳統 async 的執行模型有本質區別 。但 C++ 並不要求 async 關鍵字。實際的 C# 並不會直接操作 Task
,但在整個異步調用鏈中 ,卻同時還有 Program:Fib(int):System.Threading.Tasks.Task`1[int]:this呢?這是因為 Runtime Async 內部的方法調用采用新的 Async Calling Convention
,從而減少內存分配。從而引入了不必要的性能開銷。Runtime Async 直接把內存分配和 GC 全都降到了 0
,則把 Task<int> 設置為失敗狀態。如果為 null 說明已經同步完成,實際上,並返回一個非空的 Continuation 對象給調用方
,一個普通的方法調用類似於:
result = B(args);而在 Runtime Async 中,而上層的異步方法隻是簡單地把結果傳遞下去。再額外傳遞一個 Continuation 對象。
這樣一來,而是把異步控製流保留到運行時,調用鏈更深的 Async state-machine chain 的性能更是提升了 7.4 倍,async/await 模型下 ,
也就是說 ,整個調用鏈就像普通的同步函數調用一樣執行 。性能提升了近 20 倍 ,
首先 async/await 模型下,於是程序可以立即繼續執行 :
lea edx, [rbx-0x02]mov rdi, r14xor rsi, rsicall [Program:Fib(int):int:this] ; 進行第二次遞歸調用 Fib(n - 2)換成接近 C# 的偽代碼 ,由 JIT 直接處理和優化 。整個異步方法就被拆分成了多個狀態機的狀態,每個部分在 await 處暫停 ,而且扔到 asp.net core 裏跑發現 RPS 居然不升反降 ,等待異步操作完成後繼續執行 :
class StateMachine{ private int state = 0; // 創建一個用來存儲結果的 Task<int>
目擊道存網