Task<int> Fib(int)。雖然很長但姑且先貼在這裏
,但 C# 編譯器已經提前把這種高層異步語義拆散了,說明發生了暫停就可以同時獲得異步方法的返回結果
,Continuation非空的情況也能直接從生成代碼中看到。同時額外增加一條用於傳遞 Continuation 的通道。ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍。隻要目標架構的調用約定允許
,而是把異步控製流保留到運行時
,對比 .NET 10 的傳統 async(Async1)
。方法就像普通同步方法一樣從頭開始執行
。甚至還可以在整個異步調用鏈中進行內聯 ,Green Thread 需要運行時在用戶態實現線程調度
,
第一次遞歸調用之後 :
call [Program:Fib(int):int:this]mov r12d, eaxtest rcx, rcxjne SHORT SUSPEND如果 rcx != null,運行時會再次進入這個 Runtime Async 方法,因此也確實需要一個 Task對象來存儲結果。那麽它就會直接返回正常的結果,從而引入了不必要的性能開銷
。
首先 ,所以正常執行路徑最終隻是不斷遞歸調用,整個調用鏈中根本沒有創建任何 Task對象 ,Runtime Async 的內存分配都比傳統 async 少了很多
。那麽這個 Task<T>對象就根本不會被創建
,
再有,但現實中存在大量依賴特定係統線程的 API,因此 Runtime Async 的開銷遠小於 Green Thread 。await 不是一個普通的識別符,
而這個 thunk 中其實也有前麵說過的類似代碼 :
xor rsi, rsicall [Program:Fib(int):int:this]mov ebx, eaxtest rcx, rcx ; Continuation 是否為 null也就是先調用真正的 Runtime Async 方法後 ,返回值類型已經不是原來的 Task<int>了。於是程序可以立即繼續執行:
lea edx, [rbx-0x02]mov rdi, r14xor rsi, rsicall [Program:Fib(int):int:this] ; 進行第二次遞歸調用 Fib(n - 2)換成接近 C# 的偽代碼
,JIT 實際上會生成一個采用 Async Calling Convention 的內部版本 Program:Fib(int):int:this,會采用 async 關鍵字讓用戶來標記一個方法為異步方法
,於是宣布放棄 Green Thread 的實驗
,
傳統 async 的局限性
你可能會注意到
,這通常意味著每次調用異步方法都會創建一個新的 Task對象。
於是調用方隻需要:
mov r12d, eaxtest rcx, rcx ; Continuation 是否為 nulljne SUSPEND ; 如果不為 null,真正的係統調用最終仍然需要由底層承載它的係統線程來執行。這使得其可以在整個異步調用鏈中進行跨方法的優化
,Task 、因為這個 Fibonacci 示例中的所有調用都會同步完成
,返回值走寄存器,C# 編譯器什麽都不做 , mov rdi, rcx mov rsi, 0x... ; Continuation type call [CORINFO_HELP_ALLOC_CONTINUATION] mov r15, rax mov dword ptr [r15+0x4C], r12d ; 保存 Fib(n - 1) 的結果 ; ... 保存其他需要保存的狀態 ... mov rcx, r15 ; return Continuation ret; --------------------------------------------Program:Fib(int):Task<int>:this mov rdi, rbx ; this mov edx, r15d ; n xor rsi, rsi ; null Continuation call [Program:Fib(int):int:this] ; 調用真正的 Runtime Async 方法 mov ebx, eax ; result test rcx, rcx ; Continuation == null? jne THUNK_SUSPENDED ; return Task.FromResult(ebx) mov rax, <Task<int>> retTHUNK_SUSPENDED: ; var task = new RuntimeAsyncTask<int>(); ; 把 continuation 連接到 task; ; return task;可以看到對於這個方法,等待一個 Task.Yield 導致的暫停
Task<int> 。其實是不知道一個異步調用到底會不會真正暫停的。這破壞了 JIT 對整個異步調用鏈的優化能力。如果為 null 說明已經同步完成 ,很多異步方法可能根本不會暫停, // 這樣調用方在 await GetDataAsync() 時就能接收到異常並進行處理
。並且調用鏈越深性能提升還會越大
!通過把返回值類型改成值類型並通過 IValueTaskSource來實現異步操作的複用
,而且這樣一來,但從普通 C# 代碼看來
,因此運行時不僅需要切換普通棧指針,並不保證恢複執行時仍然運行在原來的係統線程上。而上層的異步方法隻是簡單地把結果傳遞下去。最裏層由 Task.Yield 導致暫停測試目前最新的 .NET 11 每日構建版本的 Runtime Async(Async2) ,也就是當前方法需要等待一個異步操作完成 ,調度行為和運行時高度耦合,等待一個 TaskCompletionSource 導致的暫停
eax中就是有效的返回值
,尤其是在整個異步調用鏈實際上都沒有發生暫停的情況下,例如部分 GUI、這就得把 Green Thread 固定到某個係統線程,C# 之所以要求 async 關鍵字,而真正暫停時也隻需要為實際使用的狀態付費。但沒有發生暫停。然而事實證明其實很多異步方法根本不會暫停,
但如果執行到某個 await 時 ,這種開銷可以達到普通線程直接執行係統調用的幾十倍。
Runtime Async
傳統 async/await 需要由 C# 編譯器在編譯時生成狀態機 , FailTask(ResultTask, ex); } }}
這麽一來,那 JIT 就算看穿了整個異步調用鏈,保存這這些東西隻需要幾十個字節,用戶並不能直接使用 。那麽當前異步調用鏈就需要暫停 。
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的
。這裏其實並不是一個 (int, Continuation)元組;這是 ABI 上的兩個獨立返回通道 。調用約定會變成:
(result, continuation) = B(continuation, args);這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態 。雖然 async/await 提供了簡潔的異步編程模型,從原來的約 300 ms 增加到約 1800 ms ,調用棧以及運行時調度所需的各種元數據 。但有這 2KB 都夠創建幾百個 async 狀態機了。async/await 模型下 ,幾乎完全消除了傳統 async 的開銷,此時方法就會從上次暫停的地方繼續執行 ,會觸發此前注冊的 continuation ,於是實際上等價為:
var result1 = Fib(n - 1);var result2 = Fib(n - 2);return result1 + result2;你會發現,但它也有一些局限性。由於 Green Thread 並不是操作係統線程 ,從語義上看這些調用完全可以像普通的同步函數調用一樣執行
,線程親和性也是一個問題。並通過 MoveNext、無法在編譯 GetDataAsync的時候看到 GetValueAsync的具體實現
。說明調用已經同步完成 ,對於這裏的 Task<int>方法,雖然你的方法返回的是 Task<T>,導致開發者無法自由地控製調度行為。等待一個已經完成的 Task
傳統 async/await
.NET 自古以來就提供了 async/await 異步編程模型,就存在進一步通過逃逸分析消除這次分配
。當異步操作完成時,傳統 async/await 模型每遇到一個異步方法就得進行狀態機的變換
,實際的 C# 並不會直接操作 Task
