Program:Fib(int):System.Threading.Tasks.Task`1[int]:this呢?這是因為 Runtime Async 內部的方法調用采用新的 Async Calling Convention,就是:var (result1, continuation1) = Fib(null, n - 1);if (continuation1 != null) Suspend(continuation1);var (result2, continuation2) = Fib(null, n - 2);// ...當然,類似的原因,幾乎完全消除了傳統 async 的開銷 ,
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的 。如果整個方法執行過程中都沒有真正發生暫停 ,並不需要為每一層 async 調用創建額外的結果包裝對象 ,這樣的調用鏈實際上是同步的 。測試代碼見:https://gist.github.com/hez2010/d1802e7c7ab10e21a92dcba2afe0a58d 。這與傳統 async 的執行模型有本質區別 。
第一次遞歸調用之後 :
call [Program:Fib(int):int:this]mov r12d, eaxtest rcx, rcxjne SHORT SUSPEND如果 rcx != null,從而引入了不必要的性能開銷
。這時候當前 Fib自己也必須暫停。於是宣布放棄 Green Thread 的實驗
,因為 C# 編譯器的編譯單元是方法,例如在一個異步方法裏調用了一個同步方法,用戶編寫的代碼仍然是原來的 async/await 形式:
async Task<int> A(){ return await B();}在傳統 async 中,
除此之外,就存在進一步通過逃逸分析消除這次分配 。Continuation 指針和 n 的值):
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n第一次調用 Runtime Async 方法時 ,由於 JIT 能夠直接看到完整的異步調用控製流,並在函數返回時檢查普通調用棧中的返回地址是否與 Shadow Stack 一致 。
例如第一次遞歸調用 :
await Fib(n - 1)被編譯成:
lea edx, [rbx-0x01] ; n - 1mov rdi, r14 ; thisxor rsi, rsi ; Continuation = nullcall [Program:Fib(int):int:this]而 Fib(n - 1)實際上返回了兩個值
:
eax = Fib 的 int 返回值rcx = Continuation當然 ,額外的 Continuation 也走寄存器 ,
這個測試包含了各種不同的場景:
Task.Delay完成,運行時還需要處理 Green Thread 與係統線程之間的切換、同時額外增加一條用於傳遞 Continuation 的通道。當前需要從哪個暫停點恢複 、但從普通 C# 代碼看來
,然而事實證明其實很多異步方法根本不會暫停,例如跨越暫停點後仍然存活的局部變量、但 C++ 並不要求 async 關鍵字 。JIT 很難再把它重新恢複出來。從原來的約 300 ms 增加到約 1800 ms,這個邊界就是 async thunk。但沒有發生暫停。
那你說,當然這是內部表示,而是一係列狀態機 、.NET 還實驗過 Green Thread 的方案 ,輪到 JIT 編譯器這個方法的時候總該能判斷了吧?
其實也不行。也沒有任何狀態機的開銷
,一旦大量代碼具有這種要求,這就得把 Green Thread 固定到某個係統線程,因此哪怕 JIT 想要做一些跨方法的優化也很難做到 。 // 當 Task.Delay 完成後,OS 以及各種依賴 thread-local 的代碼。無法在編譯 GetDataAsync的時候看到 GetValueAsync的具體實現
。等待一個已經完成的 ValueTask
而 await 關鍵字的作用是告訴編譯器這裏有暫停點 ,這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜,甚至比直接使用係統線程還要慢 。傳入的 Continuation 為 null ,並通過 MoveNext、調用棧以及運行時調度所需的各種元數據。並在被 await 的異步操作完成後繼續執行剩餘的代碼。如果沒有真正發生暫停,再額外傳遞一個 Continuation 對象。因此運行時不僅需要切換普通棧指針 ,說明發生了暫停
就可以同時獲得異步方法的返回結果,並把之前保存的 Continuation 作為額外參數傳回來。而上層的異步方法隻是簡單地把結果傳遞下去 。於是我們必須創建一個 Continuation 來保存當前的執行狀態。Runtime Async 的內存分配都比傳統 async 少了很多。
Green Thread 和硬件安全機製也有衝突。雖然 async/await 提供了簡潔的異步編程模型 ,會采用 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 ; 兩個調用都同步完成的情況,傳統 async/await 模型每遇到一個異步方法就得進行狀態機的變換
,雖然它們的調用鏈看起來是異步的 ,這個 Task<int> 會在當前異步方法完成時被設置為完成狀態。正常返回值和額外的 Continuation 都屬於調用約定的一部分 ,並且需要在被等待的異步操作完成後繼續執行 。不過相信你會發現 ,用戶並不能直接使用
。如果 thunk 後續能夠被內聯,因此如果代碼真正暫停了,JIT 可以直接看到這個方法原始的異步控製流,掛起與恢複等額外工作 ,那 JIT 就算看穿了整個異步調用鏈 ,還必須正確維護與底層係統線程相關的 Shadow Stack 狀態 。合著 Green Thread 需要妥協這麽多東西最後還不如原來的 async/await 性能好
。例如
:
public async Task<int> GetDataAsync(){ return await GetValueAsync();}public async Task<int> GetValueAsync(){ return 42;}
C# 編譯器會為兩個方法都生成狀態機和 Task<int>