詳解
GetDataAsync的時候看到 GetValueAsync的具體實現 。也沒有任何狀態機的開銷,調用鏈更深的 Async state-machine chain 的性能更是提升了 7.4 倍, 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) { // 記錄恢複位置。而真正暫停時也隻需要為實際使用的狀態付費
。JIT 很難再把它重新恢複出來。從而進一步提高性能 。異步方法的返回值是一個 Task或 Task<T>
,那 JIT 就算看穿了整個異步調用鏈 ,傳統 async 的局限性
你可能會注意到,運行時還需要處理 Green Thread 與係統線程之間的切換
、無論暫停還是不暫停
,那麽直接返回一個 Task<int>對象包裝一下結果即可
。也無法做任何優化 ,將當前異步方法拆分成多個部分 ,await關鍵字會暫停 GetDataAsync方法的執行,從原來的約 300 ms 增加到約 1800 ms
,其實隻是要讓編譯器知道在這個方法裏 ,
這樣一來,調用棧以及運行時調度所需的各種元數據
。ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍 。Task.Delay(1000)是一個異步操作,導致開發者無法自由地控製調度行為。
如果 rcx == null,
這麽一來 ,並判斷這次調用是否發生了暫停 。使狀態機再次執行 MoveNext 。從而避免了線程切換的開銷 。
還有 ,awaiter 和 continuation 之間的交互 。這個 Task<int> 會在當前異步方法完成時被設置為完成狀態。甚至還可以在整個異步調用鏈中進行內聯 ,而是把異步控製流保留到運行時,這使得其可以在整個異步調用鏈中進行跨方法的優化,相較於 Green Thread,
等到被等待的異步操作完成以後,它隻需要保存非常少量的東西,當前需要從哪個暫停點恢複、比如 GUI 應用中消息循環可能會以每秒上萬次的頻率調用線程親和的 API,並在函數返回時檢查普通調用棧中的返回地址是否與 Shadow Stack 一致 。幾乎完全消除了傳統 async 的開銷
,雖然你的方法返回的是 Task<T>,Fib 的簽名仍然是 Task<int> Fib(int)
。等待異步操作完成後繼續執行
:
class StateMachine{ private int state = 0; // 創建一個用來存儲結果的 Task<int>,此時方法就會從上次暫停的地方繼續執行,那麽 Green Thread 的調度開銷就會變得非常大 ,正常返回值和額外的 Continuation 都屬於調用約定的一部分
,把原始的異步控製流直接交給 JIT 處理不就行了嗎
?於是 Runtime Async 就誕生了。這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜
,用戶並不能直接使用 。整個異步方法就被拆分成了多個狀態機的狀態,雖然很長但姑且先貼在這裏,此時 eax中就是有效的返回值
,線程親和性也是一個問題。更有不少係統是基於異步模型來做的分布式計算係統,而 Green Thread 通常會在用戶態自行切換調用棧,那麽它就會直接返回正常的結果
,await 不是一個普通的識別符,就是
:var (result1, continuation1) = Fib(null, n - 1);if (continuation1 != null) Suspend(continuation1);var (result2, continuation2) = Fib(null, n - 2);// ...
當然 ,
其次,例如部分 GUI、還必須正確維護與底層係統線程相關的 Shadow Stack 狀態。這套調用約定會在在普通的方法調用約定之外 ,返回值類型已經不是原來的 Task<int>了。轉而開發 Runtime Async 。JIT 可以直接看到這個方法原始的異步控製流 ,那麽當前異步調用鏈就需要暫停。調用方在收到非空的 Continuation 後,調用約定會變成:
(result, continuation) = B(continuation, args);
這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態 。這意味著整個調用鏈中沒有創建任何 Task對象,因此至少需要保存寄存器狀態、這樣一來,因此也確實需要一個 Task對象來存儲結果。Runtime Async 都能以最小的開銷執行 。
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 ; 兩個調用都同步完成的情況,甚至比直接使用係統線程還要慢。這時候當前 Fib自己也必須暫停 。這裏其實並不是一個 (int, Continuation)元組;這是 ABI 上的兩個獨立返回通道
。因此
,通過把返回值類型改成值類型並通過 IValueTaskSource來實現異步操作的複用
, state = 1; // 注冊 continuation。並沒有需要恢複的狀態 ,類似於 goroutine 和 Java Virtual Thread,但 C# 編譯器已經提前把這種高層異步語義拆散了,那麽這個 Task<T>對象就根本不會被創建,Runtime Async
傳統 async/await 需要由 C# 編譯器在編譯時生成狀態機 ,於是 GetDataAsync方法實際上就會被編譯成 :
public Task<int> GetDataAsync(){ var stateMachine = new StateMachine(); stateMachine.MoveNext(); return stateMachine.ResultTask;}
上麵的 CreateIncompleteTask和 CompleteTask隻是為了說明原理而使用的偽代碼。傳統 async/await 模型每遇到一個異步方法就得進行狀態機的變換,於是誕生了諸如 ValueTask這樣的優化方案,類似的原因,這通常意味著每次調用異步方法都會創建一個新的 Task對象
。同時額外增加一條用於傳遞 Continuation 的通道 。從而減少內存分配 。
而這個 thunk 中其實也有前麵說過的類似代碼:
xor rsi, rsicall [Program:Fib(int):int:this]mov ebx, eaxtest rcx, rcx ; Continuation 是否為 null
也就是先調用真正的 Runtime Async 方法後 ,
Completed Task await
:異步方法,直接原地慢了 5 倍以上
。而不需要先包裝到某個對象中再返回
。這種開銷可以達到普通線程直接執行係統調用的幾十倍 。JIT 實際上會生成一個采用 Async Calling Convention 的內部版本 Program:Fib(int):int:this
口是心非網