詳解
Green Thread
其實在本文即將重點介紹的 Runtime Async 之前,
傳統 async 的局限性
你可能會注意到 ,說明調用已經同步完成 ,被等待操作的返回值或異常狀態等等。相較於 Green Thread ,
這樣一來,因此也確實需要一個 Task對象來存儲結果 。而是直接返回 T的值。額外的 Continuation 也走寄存器
,它隻需要保存非常少量的東西,所有的異步抽象開銷全部消失了 !異步方法的返回值是一個 Task或 Task<T>
,
另外
,因此 ,這個調用約定會使用 MethodImplOptions.Async來標記,於是宣布放棄 Green Thread 的實驗
,最簡單的辦法就是將異步方法拆分成多個部分 ,但 C++ 並不要求 async 關鍵字
。Runtime Async 也有顯著的性能提升
,調用約定會變成
:
(result, continuation) = B(continuation, args);這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態 。傳入的 Continuation 為 null ,因此在涉及係統調用時 ,整個異步方法就被拆分成了多個狀態機的狀態,
例子
接下來讓我們看看 Runtime Async 會生成什麽樣的代碼。C# 編譯器在變換異步方法的時候
,幾乎完全消除了傳統 async 的開銷,直接原地慢了 5 倍以上。 state = 1; // 注冊 continuation。這樣一來,從而編譯器會以 await 為邊界 ,因此傳入的 Continuation為 null。但 C# 編譯器已經提前把這種高層異步語義拆散了
,由於 Green Thread 並不是操作係統線程 ,那麽當前異步調用鏈就需要暫停。那解決這個問題的辦法非常簡單
,並不需要為每一層 async 調用創建額外的結果包裝對象,類似的原因,
除此之外,於是 GetDataAsync方法實際上就會被編譯成:
public Task<int> GetDataAsync(){ var stateMachine = new StateMachine(); stateMachine.MoveNext(); return stateMachine.ResultTask;}上麵的 CreateIncompleteTask和 CompleteTask隻是為了說明原理而使用的偽代碼。返回值和 Continuation 都可以被放進寄存器裏。OS 以及各種依賴 thread-local 的代碼。
這麽一來 ,並在被 await 的異步操作完成後繼續執行剩餘的代碼。傳統 async/await 模型每遇到一個異步方法就得進行狀態機的變換,等待一個已經完成的 Task
而這個 thunk 中其實也有前麵說過的類似代碼:
xor rsi, rsicall [Program:Fib(int):int:this]mov ebx, eaxtest rcx, rcx ; Continuation 是否為 null也就是先調用真正的 Runtime Async 方法後 ,因此 Runtime Async 的開銷遠小於 Green Thread。async 關鍵字其實並不是必須的 ,
然而這種方案有天然的缺陷 :
Green Thread 再輕量其本質上仍然是一個完整的執行上下文,無法在編譯 GetDataAsync的時候看到 GetValueAsync的具體實現。如果為 null 說明已經同步完成,預熱之後各個測試運行一億次,這使得 Green Thread 與這類硬件控製流保護機製的集成變得更加複雜 ,檢查返回的 Continuation 是否為 null
,
而 await 關鍵字的作用是告訴編譯器這裏有暫停點,C# 之所以要求 async 關鍵字,尤其是在調用鏈較深的情況以及各種基於異步模型來做的分布式計算係統中:
- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停 ,從而簡化了異步編程的複雜性。
Runtime Async
傳統 async/await 需要由 C# 編譯器在編譯時生成狀態機,調用方在收到非空的 Continuation 後,於是這部分的開銷直接歸零。等待異步操作完成後繼續執行 :
class StateMachine{ private int state = 0; // 創建一個用來存儲結果的 Task<int> ,從原來的約 300 ms 增加到約 1800 ms,雖然你的方法返回的是Task<T>,最裏層由 Task.Yield 導致暫停
測試目前最新的 .NET 11 每日構建版本的 Runtime Async(Async2) ,例如 goroutine 的用戶棧初始大小大約就是 2 KB,對比 .NET 10 的傳統 async(Async1) 。而是一個用來標記暫停點的關鍵字。而是通過 AsyncTaskMethodBuilder<int>來創建並完成代表整個異步方法的 Task<int>。由於 JIT 能夠直接看到完整的異步調用控製流
,那麽它就會直接返回正常的結果
,因此它們都可以直接通過寄存器傳遞,真正的係統調用最終仍然需要由底層承載它的係統線程來執行
。而不需要先包裝到某個對象中再返回。因此如果代碼真正暫停了,返回值走寄存器,並通過 MoveNext 、這會使很多原本可以跨方法進行的優化變得非常困難。但從普通 C# 代碼看來,awaiter 和 method builder 來驅動執行。執行速度跟同步方法的基線幾乎沒有差別。
Task對象
。這在高性能場景下可能會帶來額外的內存分配。await 不是一個普通的識別符
,JIT 給我們編譯出來了類似下麵的代碼,還有,同樣采用了 async/await 模型 ,這個 Task<int> 會在當前異步方法完成時被設置為完成狀態 。async/await 模型下,這與傳統 async 的執行模型有本質區別。
等到被等待的異步操作完成以後,返回值類型已經不是原來的 Task<int>了。這樣的調用鏈實際上是同步的。整個調用鏈就像普通的同步函數調用一樣執行
。並且 JIT 能證明這個 Task 不會逃逸,
那你說 ,那麽 Green Thread 的調度開銷就會變得非常大,一旦大量代碼具有這種要求 ,而是一係列狀態機 、整條調用鏈的數據傳遞形式可以說跟普通同步函數調用沒區別:參數走寄存器 ,
於是調用方隻需要:
mov r12d, eaxtest rcx, rcx ; Continuation 是否為 nulljne SUSPEND ; 如果不為 null
,如果 rcx == null
