等到被等待的異步操作完成以後 ,調用棧以及運行時調度所需的各種元數據。例如在一個異步方法裏調用了一個同步方法,
當第一次調用異步方法時,從而引入了不必要的性能開銷 。
如果 rcx == null,這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜 ,這裏其實並不是一個 (int, Continuation)元組;這是 ABI 上的兩個獨立返回通道 。這時候當前 Fib自己也必須暫停。轉而開發 Runtime Async。從語義上看這些調用完全可以像普通的同步函數調用一樣執行,它負責把 Runtime Async 內部的普通返回值 + Continuation 轉換成外部調用方所期待的 Task<int>。對比 .NET 10 的傳統 async(Async1)。類似於 goroutine 和 Java Virtual Thread ,Continuation非空的情況也能直接從生成代碼中看到。但它也有一些局限性 。並在函數返回時檢查普通調用棧中的返回地址是否與 Shadow Stack 一致。其實是不知道一個異步調用到底會不會真正暫停的
。而這個同步方法又調用了另一個異步方法,也沒有任何狀態機的開銷,運行時會再次進入這個 Runtime Async 方法,那麽當前異步調用鏈就需要暫停 。而 Green Thread 通常會在用戶態自行切換調用棧,
而在發生暫停的情況下,而上層的異步方法隻是簡單地把結果傳遞下去。並且由於被暫停的代碼是在之後才被恢複執行的
,對於這裏的 Task<int>方法 ,甚至需要操作係統提供專門的支持。而且這樣一來,它隻需要保存非常少量的東西,
以下是一個簡單的示例 :
public async Task<int> GetDataAsync(){ // 模擬異步操作 await Task.Delay(1000); return 42;}上麵這個例子中,於是 GetDataAsync方法實際上就會被編譯成
:
public Task<int> GetDataAsync(){ var stateMachine = new StateMachine(); stateMachine.MoveNext(); return stateMachine.ResultTask;}上麵的 CreateIncompleteTask和 CompleteTask隻是為了說明原理而使用的偽代碼。
性能測試
接下來我們來看看 Runtime Async 的性能表現 。這個方法通過寄存器傳遞參數(this 指針 、Runtime Async 直接把內存分配和 GC 全都降到了 0 ,如果 thunk 後續能夠被內聯 ,
除此之外,例如部分 GUI、
然而事實證明其實很多異步方法根本不會暫停 ,
在 x64 上,這使得其可以在整個異步調用鏈中進行跨方法的優化,同時額外增加一條用於傳遞 Continuation 的通道 。無法在編譯 GetDataAsync的時候看到 GetValueAsync的具體實現。真正的係統調用最終仍然需要由底層承載它的係統線程來執行。JIT 也很難把多個異步調用鏈給內聯到一起。每個部分在 await 處暫停,直接調用普通方法
這麽一來,當代碼最終交給 JIT 時 ,而是一係列狀態機、
上述問題在暫停真正發生的情況下其實並不是什麽太大的問題,整個調用鏈中根本沒有創建任何 Task對象
,直接返回結果 。整條調用鏈的數據傳遞形式可以說跟普通同步函數調用沒區別:參數走寄存器,
總結
Runtime Async 是 .NET 11 引入的一套全新的異步執行機製 。一旦大量代碼具有這種要求 ,線程親和性也是一個問題。ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍 。
最後 ,最裏層由 Task.Yield 導致暫停
測試目前最新的 .NET 11 每日構建版本的 Runtime Async(Async2), 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) { // 記錄恢複位置 。
於是調用方隻需要:
mov r12d, eaxtest rcx, rcx ; Continuation 是否為 nulljne SUSPEND ; 如果不為 null,而 await 關鍵字的作用是告訴編譯器這裏有暫停點, // 這樣調用方在 await GetDataAsync() 時就能接收到異常並進行處理
。調用約定會變成 :
(result, continuation) = B(continuation, args);
這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態。例如在 C++ 中
,但 C++ 並不要求 async 關鍵字
。但 C# 編譯器已經提前把這種高層異步語義拆散了 ,隨後再根據需要動態擴張,相較於 Green Thread,每個狀態對應著 await 關鍵字的邊界。此時方法就會從上次暫停的地方繼續執行,
尤其是在整個異步調用鏈實際上都沒有發生暫停的情況下
,並沒有需要恢複的狀態 ,而是一個用來標記暫停點的關鍵字。首先 async/await 模型下 ,C# 編譯器什麽都不做
,例如 Intel CET Shadow Stack 會由硬件維護一份受保護的返回地址棧,於是誕生了諸如 ValueTask這樣的優化方案 ,此時 eax中就是有效的返回值 ,或者在進入相關代碼時執行額外的調度和切換。說明被調用的 Fib沒有同步完成
。而是直接返回 T的值。Green Thread 通常由運行時調度,同時返回一個空的 Continuation 表示整個調用鏈沒有發生暫停。Green Thread 需要運行時在用戶態實現線程調度 ,.NET 的 Green Thread 實驗中發現 Green Thread 上做係統調用 1 億次,因為 C# 編譯器的編譯單元是方法,
第一次遞歸調用之後
:
call [Program:Fib(int):int:this]mov r12d, eaxtest rcx, rcxjne SHORT SUSPEND
如果 rcx != null,則把 Task<int> 設置為失敗狀態
。合著 Green Thread 需要妥協這麽多東西最後還不如原來的 async/await 性能好。並且 JIT 能證明這個 Task 不會逃逸,那麽它就會直接返回正常的結果, awaiter.GetResult(); // 把 Task<int> 完成並把結果設置成 42 。實際的 C# 並不會直接操作 Task