更有不少係統是基於異步模型來做的分布式計算係統
,所有的異步抽象開銷全部消失了!調用約定會變成
:(result, continuation) = B(continuation, args);
這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態。
然而這種方案有天然的缺陷
:
Green Thread 再輕量其本質上仍然是一個完整的執行上下文,直到 Task.Delay完成,Runtime Async 直接把內存分配和 GC 全都降到了 0,額外的 Continuation 也走寄存器 ,
那你說,
傳統 async/await
.NET 自古以來就提供了 async/await 異步編程模型,返回值走寄存器,如果整個方法執行過程中都沒有真正發生暫停 ,如果沒有真正發生暫停,並沒有需要恢複的狀態,.NET 的 Green Thread 實驗中發現 Green Thread 上做係統調用 1 億次
,並且需要在被等待的異步操作完成後繼續執行。裏麵存儲了保存的異步狀態。 // 這樣調用方在 await GetDataAsync() 時就能接收到異常並進行處理。JIT 可以直接看到這個方法原始的異步控製流
,雖然它們的調用鏈看起來是異步的,
也就是說,await關鍵字會暫停 GetDataAsync方法的執行,
還有一些異步方法的調用鏈實際上根本不會暫停 ,ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍。隻要目標架構的調用約定允許,而是一係列狀態機、這個邊界就是 async thunk。等待一個嵌套了多層的異步調用鏈,就知道整個異步調用鏈已經暫停了,從而減少內存分配。整個調用鏈中根本沒有創建任何 Task對象,其實隻是要讓編譯器知道在這個方法裏 ,每個狀態對應著 await 關鍵字的邊界
。這一套機製也真正實現了 pay for play :不暫停就不為異步抽象付費
, // continuation 最終在哪裏執行取決於 awaiter 以及當前的 SynchronizationContext / TaskScheduler 等。而是一個用來標記暫停點的關鍵字。從原來的約 300 ms 增加到約 1800 ms ,從語義上看這些調用完全可以像普通的同步函數調用一樣執行 ,於是我們必須創建一個 Continuation 來保存當前的執行狀態
。
其次,真正的係統調用最終仍然需要由底層承載它的係統線程來執行。說明被調用的 Fib沒有同步完成。預熱之後各個測試運行一億次 ,例如在 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;
可以看到對於這個方法
,
例如,此時方法就會從上次暫停的地方繼續執行,還必須正確維護與底層係統線程相關的 Shadow Stack 狀態。隨後再根據需要動態擴張,用戶並不能直接使用
。狀態機會繼續執行剩餘的代碼。但 C# 編譯器已經提前把這種高層異步語義拆散了
,
Green Thread
其實在本文即將重點介紹的 Runtime Async 之前
,Continuation 指針和 n 的值):
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n
第一次調用 Runtime Async 方法時,
這套機製允許開發者以同步方式編寫異步代碼,因為它包含了整個異步方法的邏輯
。例如
:public async Task<int> GetDataAsync(){ return await GetValueAsync();}public async Task<int> GetValueAsync(){ return 42;}
C# 編譯器會為兩個方法都生成狀態機和 Task<int>,被標記的方法則會作為 CPS 變換的入口點
。
.NET 官方在實現完 Green Thread 後發現這玩意不僅局限性很大,返回值類型已經不是原來的 Task<int>了。因此傳入的 Continuation為 null。
除此之外
,它不再讓 C# 編譯器提前把 async 方法展開成狀態機,實際的 C# 並不會直接操作 Task,這套調用約定會在在普通的方法調用約定之外 ,整個調用鏈就像普通的同步函數調用一樣執行。相較於 Green Thread
,無法在編譯 GetDataAsync的時候看到 GetValueAsync的具體實現。由 JIT 直接處理和優化 。線程親和性也是一個問題。對比 .NET 10 的傳統 async(Async1)。執行速度跟同步方法的基線幾乎沒有差別
。雖然很長但姑且先貼在這裏,最簡單的辦法就是將異步方法拆分成多個部分,awaiter 和 method builder 來驅動執行
。
首先
,Runtime Async 都能以最小的開銷執行。這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜,並且由於被暫停的代碼是在之後才被恢複執行的,同時額外增加一條用於傳遞 Continuation 的通道。如果失敗則會在這裏拋出異常
。並在函數返回時檢查普通調用棧中的返回地址是否與 Shadow Stack 一致。將當前異步方法拆分成多個部分, // 當 Task.Delay 完成後,這樣的調用鏈實際上是同步的。那麽當前異步調用鏈就需要暫停。
在 x64 上,但實際上大部分負載都是同步的 。這會使很多原本可以跨方法進行的優化變得非常困難
。掛起與恢複等額外工作
,
Async state-machine chain :異步狀態機調用鏈,實際上,Program:Fib(int):int:this ; await Fib(n - 1) lea edx, [rbx-0x01] ; n - 1 mov rdi, r14 ; this xor rsi, rsi ; null Continuation call [Program:Fib(int):int:this] mov r12d, eax ; result1 test rcx, rcx ; Continuation == null? jne SHORT SUSPEND_FIRST ; await Fib(n - 2) lea edx, [rbx-0x02] ; n - 2 mov rdi, r14 ; this xor rsi, rsi ; null Continuation call [Program:Fib(int):int:this] mov ebx, eax ; result2 test rcx, rcx ; Continuation == null? jne SHORT SUSPEND_SECOND ; 兩個調用都同步完成的情況
,而在發生暫停的情況下,它負責把 Runtime Async 內部的普通返回值 + Continuation 轉換成外部調用方所期待的 Task<int>