Task.Delay(1000)是一個異步操作 ,因此如果代碼真正暫停了 ,當異步操作完成時
, awaiter.GetResult(); // 把 Task<int> 完成並把結果設置成 42。它不再讓 C# 編譯器提前把 async 方法展開成狀態機,最終
,雖然它們的調用鏈看起來是異步的,這意味著整個調用鏈中沒有創建任何 Task對象,這時候當前 Fib自己也必須暫停。從而減少內存分配。一旦大量代碼具有這種要求,此時運行時會保存繼續執行所需要的狀態
,
在 x64 上 ,如果為 null 說明已經同步完成 ,Runtime Async 直接把內存分配和 GC 全都降到了 0, // 這樣調用方在 await GetDataAsync() 時就能接收到異常並進行處理 。額外的 Continuation 也走寄存器,
當第一次調用異步方法時,返回值走寄存器,
那你說 ,每個部分在 await 處暫停,而這個同步方法又調用了另一個異步方法 ,導致開發者無法自由地控製調度行為。執行速度跟同步方法的基線幾乎沒有差別。傳統 async/await 模型每遇到一個異步方法就得進行狀態機的變換,等待一個 Task.Yield 導致的暫停
Task.Delay完成,C# 之所以要求 async 關鍵字,這會使很多原本可以跨方法進行的優化變得非常困難。JIT 很難再把它重新恢複出來 。當然,等到被等待的異步操作完成以後,因此哪怕 JIT 想要做一些跨方法的優化也很難做到。直接原地慢了 5 倍以上 。
Green Thread
其實在本文即將重點介紹的 Runtime Async 之前,
這樣一來,無法在編譯 GetDataAsync的時候看到 GetValueAsync的具體實現 。合著 Green Thread 需要妥協這麽多東西最後還不如原來的 async/await 性能好。那麽當前異步調用鏈就需要暫停。
然而事實證明其實很多異步方法根本不會暫停,但在整個異步調用鏈中,返回值和 Continuation 都可以被放進寄存器裏。而真正暫停時也隻需要為實際使用的狀態付費。甚至比直接使用係統線程還要慢。Continuation非空的情況也能直接從生成代碼中看到。但實際上大部分負載都是同步的。於是我們必須創建一個 Continuation 來保存當前的執行狀態。這在高性能場景下可能會帶來額外的內存分配。裏麵存儲了保存的異步狀態
。從而進一步提高性能。同時額外增加一條用於傳遞 Continuation 的通道。無論暫停還是不暫停,會觸發此前注冊的 continuation
,並在被 await 的異步操作完成後繼續執行剩餘的代碼。下麵會解釋。而上層的異步方法隻是簡單地把結果傳遞下去
。
async/await 機製本質上是利用 CPS(Continuation Passing Style)變換來實現的 。如果失敗則會在這裏拋出異常。 state = 1; // 注冊 continuation 。而且扔到 asp.net core 裏跑發現 RPS 居然不升反降,真正的係統調用最終仍然需要由底層承載它的係統線程來執行。JIT 在編譯 MoveNext時通常會因為代碼體積過大而避免內聯,方法就像普通同步方法一樣從頭開始執行。直到整個異步調用鏈完成。甚至還可以在整個異步調用鏈中進行內聯
,每個狀態對應著 await 關鍵字的邊界。尤其是在整個異步調用鏈實際上都沒有發生暫停的情況下,它負責把 Runtime Async 內部的普通返回值 + Continuation 轉換成外部調用方所期待的 Task<int> 。從原來的約 300 ms 增加到約 1800 ms
,等待一個 ThreadPool 上的 continuation 導致的暫停
另外,
首先,等待一個嵌套了多層的異步調用鏈,由於 Green Thread 並不是操作係統線程,
並沒有需要恢複的狀態,而是一係列狀態機、甚至需要操作係統提供專門的支持。其實是不知道一個異步調用到底會不會真正暫停的。也就是當前方法需要等待一個異步操作完成 ,於是誕生了諸如ValueTask這樣的優化方案
,於是實際上等價為:var result1 = Fib(n - 1);var result2 = Fib(n - 2);return result1 + result2;你會發現,調度行為和運行時高度耦合 ,因此也確實需要一個 Task對象來存儲結果。也就是說,.NET 還實驗過 Green Thread 的方案
,測試代碼見:https://gist.github.com/hez2010/d1802e7c7ab10e21a92dcba2afe0a58d
。等待一個已經完成的 Task
然而這種方案有天然的缺陷:
Green Thread 再輕量其本質上仍然是一個完整的執行上下文,但有這 2KB 都夠創建幾百個 async 狀態機了。掛起與恢複等額外工作,那 JIT 就算看穿了整個異步調用鏈,C# 編譯器什麽都不做,
例如 ,C# 編譯器會把異步方法改寫成狀態機 ,線程親和性也是一個問題
。就存在進一步通過逃逸分析消除這次分配 。直接返回結果。運行時還需要處理 Green Thread 與係統線程之間的切換 、而是通過 AsyncTaskMethodBuilder<int>來創建並完成代表整個異步方法的 Task<int>。await關鍵字會暫停 GetDataAsync方法的執行,
其次,或者在進入相關代碼時執行額外的調度和切換。狀態機會繼續執行剩餘的代碼 。
如果 rcx == null
,因此傳入的 Continuation為 null