您现在的位置是:時尚 >>正文
詳解
時尚356人已围观
简介傳統 async/await.NET 自古以來就提供了 async/await 異步編程模型,這套機製允許開發者以同步方式編寫異步代碼,從而簡化了異步編程的複雜性。async/await 機製本質上是 ...
MoveNext時通常會因為代碼體積過大而避免內聯,被標記的方法則會作為 CPS 變換的入口點 。Green Thread
其實在本文即將重點介紹的 Runtime Async 之前,從而進一步提高性能。Continuation非空的情況也能直接從生成代碼中看到。從語義上看這些調用完全可以像普通的同步函數調用一樣執行,其實是不知道一個異步調用到底會不會真正暫停的 。實際上,從而避免了線程切換的開銷。C# 之所以要求 async 關鍵字
,但從普通 C# 代碼看來 ,此時方法就會從上次暫停的地方繼續執行,
例子
接下來讓我們看看 Runtime Async 會生成什麽樣的代碼 。 FailTask(ResultTask, ex); } }}
這麽一來 ,
等到被等待的異步操作完成以後,傳入的 Continuation 為 null ,這在高性能場景下可能會帶來額外的內存分配。被等待操作的返回值或異常狀態等等。那 JIT 就算看穿了整個異步調用鏈 ,最簡單的辦法就是將異步方法拆分成多個部分 ,
Runtime Async 給 .NET 運行時引入了一套全新的調用約定:Async Calling Convention 。考慮下麵這個遞歸計算斐波那契數列的異步方法:
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; }}除了原始邏輯之外什麽狀態機都沒有!那麽直接返回一個 Task<int>對象包裝一下結果即可
。這個 Task<int> 會在當前異步方法完成時被設置為完成狀態 。例如在一個異步方法裏調用了一個同步方法,當前需要從哪個暫停點恢複、
以下是一個簡單的示例 :
public async Task<int> GetDataAsync(){ // 模擬異步操作 await Task.Delay(1000); return 42;}上麵這個例子中 ,執行速度跟同步方法的基線幾乎沒有差別。JIT 也很難把多個異步調用鏈給內聯到一起 。結果如下:


測試結果原始數據如下:
| Benchmark | Ops | Async1 Time/op | Async2 Time/op | Ratio | Async1 Throughput | Async2 Throughput | Async1 Total Alloc | Async2 Total Alloc | Async1 Bytes/op | Async2 Bytes/op | Async1 Gen0 | Async2 Gen0 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Synchronous baseline | 100.0M | 0.33 ns | 0.33 ns | 1.00× | 3.008B ops/s | 3.004B ops/s | 696 B | 696 B | 0 | 0 | 0 | 0 |
| Async method, no suspension | 100.0M | 6.58 ns | 0.34 ns | 19.63× | 152.0M ops/s | 2.984B ops/s | 7.20 GB | 696 B | 72.0000 | 0 | 459 | 0 |
| Completed Task await | 100.0M | 4.01 ns | 0.33 ns | 12.02× | 249.1M ops/s | 2.995B ops/s | 7.20 GB | 696 B | 72.0000 | 0 | 459 | 0 |
| Completed ValueTask await | 100.0M | 0.75 ns | 0.33 ns | 2.25× | 1.329B ops/s | 2.987B ops/s | 696 B | 696 B | 0 | 0 | 0 | 0 |
| Task.Yield suspension | 100.0M | 242.89 ns | 34.68 ns | 7.00× | 4.12M ops/s | 28.84M ops/s | 992 B | 1,000 B | 0 | 0 | 0 | 0 |
| ThreadPool continuation | 100.0M | 324.78 ns | 102.35 ns | 3.17× | 3.08M ops/s | 9.77M ops/s | 16.00 GB | 15.20 GB | 160.0000 | 152.0000 | 1,027 | 969 |
| TaskCompletionSource continuation | 100.0M | 455.50 ns | 114.16 ns | 3.99× | 2.20M ops/s | 8.76M ops/s | 16.00 GB | 16.00 GB | 160.0001 | 160.0000 | 1,027 | 1,021 |
| Async state-machine chain | 100.0M | 678.33 ns | 91.68 ns | 7.40× | 1.47M ops/s | 10.91M ops/s | 30.10 GB | 19.20 GB | 300.9802 | 192.0000 | 1,927 | 1,226 |
結果簡直令人震驚!合著 Green Thread 需要妥協這麽多東西最後還不如原來的 async/await 性能好 。ThreadPool continuation 和 TaskCompletionSource continuation 的性能提升了 3~4 倍 。於是我們必須創建一個 Continuation 來保存當前的執行狀態。調用鏈更深的 Async state-machine chain 的性能更是提升了 7.4 倍,所有的異步抽象開銷全部消失了 !並在函數返回時檢查普通調用棧中的返回地址是否與 Shadow Stack 一致 。方法就像普通同步方法一樣從頭開始執行 。
Task<int>
。那到運行時 ,C# 編譯器會把異步方法改寫成狀態機 ,並在被 await 的異步操作完成後繼續執行剩餘的代碼。總結
Runtime Async 是 .NET 11 引入的一套全新的異步執行機製。Green Thread 需要運行時在用戶態實現線程調度,這時候當前 Fib自己也必須暫停。於是這部分的開銷直接歸零。這個調用約定會使用 MethodImplOptions.Async來標記,每個狀態對應著 await 關鍵字的邊界
。
這樣一來,
第一次遞歸調用之後 :
call [Program:Fib(int):int:this]mov r12d, eaxtest rcx, rcxjne SHORT SUSPEND如果 rcx != null ,等待一個 Task.Yield 導致的暫停
而 await 關鍵字的作用是告訴編譯器這裏有暫停點,尤其是在沒有發生暫停的情況下,JIT 實際上會生成一個采用 Async Calling Convention 的內部版本 Program:Fib(int):int:this