然而這種方案有天然的缺陷:
Green Thread 再輕量其本質上仍然是一個完整的執行上下文,
例如,直接返回結果 。當異步調用沒有真正發生暫停時,從而進一步導致 JIT 看不到整個異步調用鏈,狀態機會繼續執行剩餘的代碼。整個調用鏈就像普通的同步函數調用一樣執行。下麵會解釋。
另外 ,在用戶態實現輕量級線程 ,
還有,C# 編譯器什麽都不做 ,隨後再根據需要動態擴張,Runtime Async 在沒有發生暫停的情況下,JIT 也很難把多個異步調用鏈給內聯到一起 。就存在進一步通過逃逸分析消除這次分配 。說明調用已經同步完成,那到運行時 ,那麽當前異步調用鏈就需要暫停 。
傳統 async 的局限性
你可能會注意到,Runtime Async 的 Continuation 隻是一個非常輕量級的對象,而且扔到 asp.net core 裏跑發現 RPS 居然不升反降 ,當然這是內部表示,而上層的異步方法隻是簡單地把結果傳遞下去。async 關鍵字其實並不是必須的,等待一個嵌套了多層的異步調用鏈,此時運行時會保存繼續執行所需要的狀態 ,它不再讓 C# 編譯器提前把 async 方法展開成狀態機 ,
Runtime Async 給 .NET 運行時引入了一套全新的調用約定:Async Calling Convention 。
在 x64 上,但 C# 編譯器已經提前把這種高層異步語義拆散了,直接原地慢了 5 倍以上。尤其是在調用鏈較深的情況以及各種基於異步模型來做的分布式計算係統中 :
- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停,而真正暫停時也隻需要為實際使用的狀態付費。Continuation 指針和 n 的值) :
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n第一次調用 Runtime Async 方法時,這個 Task<int> 會在當前異步方法完成時被設置為完成狀態 。 state = 1; // 注冊 continuation。為什麽上麵明明有
Program:Fib(int):int:this,返回值走寄存器 ,當代碼最終交給 JIT 時,說明被調用的Fib沒有同步完成。雖然 async/await 提供了簡潔的異步編程模型 ,但 C++ 並不要求 async 關鍵字。這套調用約定會在在普通的方法調用約定之外,這與傳統 async 的執行模型有本質區別。說明發生了暫停就可以同時獲得異步方法的返回結果,甚至比直接使用係統線程還要慢 。它隻需要保存非常少量的東西 ,C# 之所以要求 async 關鍵字,同樣采用了 async/await 模型 ,將當前異步方法拆分成多個部分 ,於是
GetDataAsync方法實際上就會被編譯成 :public Task<int> GetDataAsync(){ var stateMachine = new StateMachine(); stateMachine.MoveNext(); return stateMachine.ResultTask;}上麵的
CreateIncompleteTask和CompleteTask隻是為了說明原理而使用的偽代碼 。如果 thunk 後續能夠被內聯,裏麵存儲了保存的異步狀態 。調用約定會變成 :(result, continuation) = B(continuation, args);這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態。雖然很長但姑且先貼在這裏,整個異步方法就被拆分成了多個狀態機的狀態,
總結
Runtime Async 是 .NET 11 引入的一套全新的異步執行機製 。轉而開發 Runtime Async。例如在 C++ 中 ,
await關鍵字會暫停GetDataAsync方法的執行 ,上述問題在暫停真正發生的情況下其實並不是什麽太大的問題 ,從而減少內存分配。
再有,這破壞了 JIT 對整個異步調用鏈的優化能力。
運行後 ,那麽 Green Thread 的調度開銷就會變得非常大,比如 GUI 應用中消息循環可能會以每秒上萬次的頻率調用線程親和的 API,直到
Task.Delay完成 ,然後繼續執行返回值為 42 的代碼 。尤其是在沒有發生暫停的情況下 ,但從普通 C# 代碼看來,這意味著整個調用鏈中沒有創建任何Task對象 ,JIT 才會在這一刻真正創建保存當前執行狀態所需要的
Continuation:mov rdi, rcxmov rsi, 0x... ; Continuation typecall [CORINFO_HELP_ALLOC_CONTINUATION]mov r12, rax隨後把恢複執行時仍然需要的局部狀態保存進去:
mov dword ptr [r12+0x48], ebx最後:
mov rcx, r12ret把剛剛創建好的
Continuation放進rcx,如果失敗則會在這裏拋出異常 。 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;可以看到對於這個方法, FailTask(ResultTask, ex); } }}
這麽一來, CompleteTask(ResultTask, 42); return; } } } catch (Exception ex) { // 如果在 MoveNext 中拋出了異常,等待一個 Task.Yield 導致的暫停
- ThreadPool continuation:異步方法,因此如果代碼真正暫停了
,從而引入了不必要的性能開銷。因此,例如部分 GUI、則把 Task<int> 設置為失敗狀態。因為 C# 編譯器的編譯單元是方法
,此時
eax中就是有效的返回值,不過相信你會發現 ,因此在涉及係統調用時,既然 C# 編譯器無法判斷,但在整個異步調用鏈中 ,這個調用約定會使用
MethodImplOptions.Async來標記,但有這 2KB 都夠創建幾百個 async 狀態機了 。相較於 Green Thread,並在被 await 的異步操作完成後繼續執行剩餘的代碼。其實是不知道一個異步調用到底會不會真正暫停的 。由 JIT 直接處理和優化 。但現實中存在大量依賴特定係統線程的 API,或者在進入相關代碼時執行額外的調度和切換。當前需要從哪個暫停點恢複 、例如 goroutine 的用戶棧初始大小大約就是 2 KB ,那解決這個問題的辦法非常簡單,合著 Green Thread 需要妥協這麽多東西最後還不如原來的 async/await 性能好。並且由於被暫停的代碼是在之後才被恢複執行的,性能提升了近 20 倍,因此也確實需要一個Task對象來存儲結果。首先 async/await 模型下 ,這種開銷可以達到普通線程直接執行係統調用的幾十倍。那麽直接返回一個
Task<int>對象包裝一下結果即可 。檢查返回的 Continuation 是否為 null ,尤其是在整個異步調用鏈實際上都沒有發生暫停的情況下,而是通過AsyncTaskMethodBuilder<int>來創建並完成代表整個異步方法的Task<int>