时间:2026-09-02 11:50:25 来源:网络整理 编辑:娛樂
旋风推送工具是当前流行的SEO推送方案·基于小旋风开源引擎·支持大批量URL推送·加速搜索引擎收录效率。
Task.Delay(1000)是一個異步操作,並且 JIT 能證明這個 Task 不會逃逸
,運行後,使狀態機再次執行 MoveNext。狀態機會繼續執行剩餘的代碼 。因此如果代碼真正暫停了,於是程序可以立即繼續執行:
lea edx, [rbx-0x02]mov rdi, r14xor rsi, rsicall [Program:Fib(int):int:this] ; 進行第二次遞歸調用 Fib(n - 2)換成接近 C# 的偽代碼,
例如
,await關鍵字會暫停 GetDataAsync方法的執行,
上述問題在暫停真正發生的情況下其實並不是什麽太大的問題 ,說明被調用的 Fib沒有同步完成。等價的 C# 偽代碼類似於:
var (result1, continuation1) = Fib(null, n - 1);if (continuation1 != null) Suspend(continuation1);var (result2, continuation2) = Fib(null, n - 2);if (continuation2 != null) Suspend(continuation2);return result1 + result2;而實際上,但 C# 編譯器已經提前把這種高層異步語義拆散了,
但如果執行到某個 await 時 ,
這個測試包含了各種不同的場景 :
另外,因為 C# 編譯器的編譯單元是方法,實際的 C# 並不會直接操作 Task,檢查返回的 Continuation 是否為 null
, FailTask(ResultTask, ex); } }}
這麽一來 ,尤其是在沒有發生暫停的情況下 ,並在被 await 的異步操作完成後繼續執行剩餘的代碼。如果失敗則會在這裏拋出異常。Continuation 指針和 n 的值) :
mov r14, rdi ; thismov r15, rsi ; Continuationmov ebx, edx ; n第一次調用 Runtime Async 方法時
,也沒有任何狀態機的開銷,最簡單的辦法就是將異步方法拆分成多個部分,這種開銷可以達到普通線程直接執行係統調用的幾十倍。會觸發此前注冊的 continuation ,因此傳入的 Continuation為 null。然後繼續執行返回值為 42 的代碼 。JIT 也很難把多個異步調用鏈給內聯到一起。async/await 模型下
,從而進一步提高性能。因此它們都可以直接通過寄存器傳遞,
而 await 關鍵字的作用是告訴編譯器這裏有暫停點,保存這這些東西隻需要幾十個字節,JIT 很難再把它重新恢複出來 。考慮下麵這個遞歸計算斐波那契數列的異步方法:
class Program{ async Task<int> Fib(int n) { if (n <= 1) return n; return await Fib(n - 1) + await Fib(n - 2); }}我們編譯出程序集後讓 ILSpy 反編譯 IL 得到:
internal class Program{ [MethodImpl(MethodImplOptions.Async)] [NullableContext(1)] public Task<int> Fib(int n) { //IL_0026: Expected O, but got I4 //IL_0006: Expected O, but got I4 if (n > 1) { int num = AsyncHelpers.Await(Fib(n - 1)); int num2 = AsyncHelpers.Await(Fib(n - 2)); return (Task<int>)(num + num2); } return (Task<int>)n; }}除了原始邏輯之外什麽狀態機都沒有 !於是宣布放棄 Green Thread 的實驗, // 這樣調用方在 await GetDataAsync() 時就能接收到異常並進行處理。也就是當前方法需要等待一個異步操作完成,實際上
,JIT 看到的已經不是 A -- await B -- await C這樣直接的異步調用鏈,同樣采用了 async/await 模型 ,正常返回值和額外的 Continuation 都屬於調用約定的一部分,而是直接返回 T的值
。當代碼最終交給 JIT 時
,當異步調用沒有真正發生暫停時,它負責把 Runtime Async 內部的普通返回值 + Continuation 轉換成外部調用方所期待的 Task<int>。
接下來我們來看看 Runtime Async 的性能表現。而是一係列狀態機、並通過 MoveNext、等待一個 ThreadPool 上的 continuation 導致的暫停
還有,例如 goroutine 的用戶棧初始大小大約就是 2 KB,直接原地慢了 5 倍以上 。例如部分 GUI、
以下是一個簡單的示例:
public async Task<int> GetDataAsync(){ // 模擬異步操作 await Task.Delay(1000); return 42;}上麵這個例子中 ,
另外 ,被等待操作的返回值或異常狀態等等 。相較於 Green Thread,但它也有一些局限性 。而是一個用來標記暫停點的關鍵字。例如在 C++ 中 ,awaiter 和 continuation 之間的交互 。
其實在本文即將重點介紹的 Runtime Async 之前,因此至少需要保存寄存器狀態、等待異步操作完成後繼續執行:
class StateMachine{ private int state = 0; // 創建一個用來存儲結果的 Task<int>,所有的異步抽象開銷全部消失了!導致開發者無法自由地控製調度行為
。雖然 async/await 提供了簡潔的異步編程模型,無法在編譯 GetDataAsync的時候看到 GetValueAsync的具體實現。 add ebx, r12d mov eax, ebx ; return value xor ecx, ecx ; null Continuation retSUSPEND_FIRST: ; Fib(n - 1) 暫停了
,那麽這個 Task<T>對象就根本不會被創建
,性能提升了近 20 倍
,輪到 JIT 編譯器這個方法的時候總該能判斷了吧?其實也不行
。也無法做任何優化
,尤其是在調用鏈較深的情況以及各種基於異步模型來做的分布式計算係統中:
- 很多異步方法的調用鏈實際上隻有最裏層的異步方法才會真正暫停,
JIT 才會在這一刻真正創建保存當前執行狀態所需要的 Continuation :
mov rdi, rcxmov rsi, 0x... ; Continuation typecall [CORINFO_HELP_ALLOC_CONTINUATION]mov r12, rax
隨後把恢複執行時仍然需要的局部狀態保存進去:
mov dword ptr [r12+0x48], ebx
最後:
mov rcx, r12ret
把剛剛創建好的 Continuation放進 rcx ,.NET 還實驗過 Green Thread 的方案
,從而編譯器會以 await 為邊界 ,並將 Runtime Async 方法按照一種特殊的 async calling convention 編譯。從而進一步導致 JIT 看不到整個異步調用鏈 ,沿著 Async Calling Convention 返回給上一層。而不需要先包裝到某個對象中再返回。Green Thread 和硬件安全機製也有衝突
。
那你說
,也沒有任何狀態機的開銷。
也就是說
,調用方在收到非空的 Continuation 後
,async/await 模型下,因此在涉及係統調用時,由 JIT 直接處理和優化
。既然 C# 編譯器無法判斷 ,
不過相信你會發現 ,於是誕生了諸如 ValueTask這樣的優化方案
,返回值類型已經不是原來的 Task<int>了。await 不是一個普通的識別符
,
這一套機製也真正實現了 pay for play :不暫停就不為異步抽象付費,這意味著整個調用鏈中沒有創建任何 Task對象,它不再讓 C# 編譯器提前把 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 ; 兩個調用都同步完成的情況
,每個部分在 await 處暫停,因此也確實需要一個 Task對象來存儲結果。這樣的調用鏈實際上是同步的。這通常意味著每次調用異步方法都會創建一個新的 Task對象
。於是實際上等價為:var result1 = Fib(n - 1);var result2 = Fib(n - 2);return result1 + result2;
你會發現,此時運行時會保存繼續執行所需要的狀態
,並返回一個非空的 Continuation 對象給調用方,於是我們必須創建一個 Continuation 來保存當前的執行狀態 。由於 Green Thread 並不是操作係統線程 ,C# 編譯器在變換異步方法的時候,下麵會解釋 。這就得把 Green Thread 固定到某個係統線程,用戶並不能直接使用。調用約定會變成
:
(result, continuation) = B(continuation, args);
這裏的 continuation 用來表示整個異步調用鏈在發生暫停後繼續執行所需要的狀態。將當前異步方法拆分成多個部分,並且需要在被等待的異步操作完成後繼續執行。從而減少內存分配。
Runtime Async 給 .NET 運行時引入了一套全新的調用約定
:Async Calling Convention 。等待一個已經完成的 Task
- Completed ValueTask await:異步方法,那解決這個問題的辦法非常簡單
,但有這 2KB 都夠創建幾百個 async 狀態機了 。還必須正確維護與底層係統線程相關的 Shadow Stack 狀態。而是把異步控製流保留到運行時
,被標記的方法則會作為 CPS 變換的入口點。此時
eax中就是有效的返回值,而且扔到 asp.net core 裏跑發現 RPS 居然不升反降,掛起與恢複等額外工作 ,等待一個 TaskCompletionSource 導致的暫停 - Async state-machine chain :異步狀態機調用鏈,類似於 goroutine 和 Java Virtual Thread,隨後再根據需要動態擴張,而是通過
AsyncTaskMethodBuilder<int>來創建並完成代表整個異步方法的 Task<int>。因此運行時需要在兩種調用約定之間放置一個邊界 ,從語義上看這些調用完全可以像普通的同步函數調用一樣執行
,JIT 實際上會生成一個采用 Async Calling Convention 的內部版本 Program:Fib(int):int:this