1. 程式人生 > >async/await 的基本實現和 .NET Core 2.1 中相關效能提升

async/await 的基本實現和 .NET Core 2.1 中相關效能提升

前言

這篇文章的開頭,筆者想多說兩句,不過也是為了以後再也不多嘴這樣的話。

在日常工作中,筆者接觸得最多的開發工作仍然是在 .NET Core 平臺上,當然因為團隊領導的開放性和團隊風格的多樣性(這和 CTO 以及主管的個人能力也是分不開的),業界前沿的技術概念也都能在上手的專案中出現。所以雖然現在團隊仍然處於疾速的發展中,也存在一些奇奇怪怪的事情,工作內容也算有緊有鬆,但是總體來說也算有苦有樂,不是十分排斥。

其實這樣的環境有些類似於筆者心中的“聖地” Thoughtworks 的 雛形(TW的HR快來找我啊),筆者和女朋友談到自己最想做的工作也是技術諮詢。此類技術諮詢公司的開發理念基本可以用一句概括:遵循可擴充套件開發,可快速迭代,可持續部署,可的架構設計,追求目標應用場景下最優於團隊的技術選型決策

所以語言之爭也好,平臺之爭也好,落到每一個對程式設計和解決問題感興趣的開發者身上,便成了最微不足道的問題。能夠感受不同技術間的碰撞,領略到不同架構思想中的精妙,就已經是一件滿足的事情了,等到團隊需要你快速應用其他技術選型時,之前的努力也是助力。當然面向工資程式設計也是一種取捨,筆者思考的時候也會陷入這個怪圈,所以希望在不斷的學習和實踐中,能夠讓自己更滿意吧。

著名的 DRY 原則告訴我們 —— Don't repeat yourself,而筆者想更進一步的是,Deep Dive And Wide Mind,深入更多和嘗試更多。

奇怪的前言就此結束。

作為最新的正式版本,雖然版本號只是小小的提升,但是 .NET Core 2.1 相比 .NET Core 2.0 在效能上又有了大大的提升。無論是專案構建速度,還是字串操作,網路傳輸和 JIT 內聯方法效能,可以這麼說的是,如今的 .NET Core 已經主動為開發者帶來摳到位元組上的節省體驗。具體的介紹還請參看 Performance Improvements in .NET Core 2.1 。

而在這篇文章裡,筆者要聊聊的只是關於 async/await 的一些底層原理和 .NET Core 2.1 在非同步操作物件分配上的優化操作。

async/await 實現簡介

熟悉非同步操作的開發者都知道,async/await 的實現基本上來說是一個骨架程式碼(Template method)和狀態機。

640?wx_fmt=png&wxfrom=5&wx_lazy=1

從反編譯器中我們可以窺見骨架方法的全貌。假設有這樣一個示例程式

internal class Program{   
 private static void Main()    {    
    var result = AsyncMethods.CallMethodAsync("async/await"
).GetAwaiter().GetResult();        Console.WriteLine(result);    } }
    
    internal static class AsyncMethods{  
    
      internal static async Task<int> CallMethodAsync(string arg)    {        var result = await MethodAsync(arg);      
        await Task.Delay(result);    
            return result;    }  
    
     private static async Task<int> MethodAsync(string arg)    {    
        var total = arg.First() + arg.Last();
        await Task.Delay(total);  
             return total;    } }

為了能更好地顯示編譯程式碼,特地將非同步操作分成兩個方法來實現,即組成了一條非同步操作鏈。這種“侵入性”傳遞對於開發其實是更友好的,當代碼中的一部分採用了非同步程式碼,整個傳遞鏈條上便不得不採用非同步這樣一種正確的方式。接下來讓我們看看編譯器針對上述非同步方法生成的骨架方法和狀態機(也已經經過美化產生可讀的C#程式碼)。

[DebuggerStepThrough]
[AsyncStateMachine((typeof(CallMethodAsyncStateMachine)]
private static Task<int> CallMethodAsync(string arg)
{
    CallMethodAsyncStateMachine stateMachine = new CallMethodAsyncStateMachine {
        arg = arg,
        builder = AsyncTaskMethodBuilder<int>.Create(),
        state = -1
    };
    stateMachine.builder.Start<CallMethodAsyncStateMachine>(
    (ref stateMachine)=>
    {
        // 骨架方法啟動第一次 MoveNext 方法
        stateMachine.MoveNext();
    });
    
    return stateMachine.builder.Task;
}

[DebuggerStepThrough]
[AsyncStateMachine((typeof(MethodAsyncStateMachine)]
private static Task<int> MethodAsync(string arg)
{
    MethodAsyncStateMachine stateMachine = new MethodAsyncStateMachine {
        arg = arg,
        builder = AsyncTaskMethodBuilder<int>.Create(),
        state = -1
    };
    
    // 恢復委託函式
    Action __moveNext = () => 
    {
        stateMachine.builder.Start<CallMethodAsyncStateMachine>(ref stateMachine);
    }
    
    __moveNext();
    
    return stateMachine.builder.Task;
}
  • MethodAsync/CallMethodAsync - 骨架方法

  • MethodAsyncStateMachine/CallMethodAsyncStateMachine - 每個 async 標記的非同步操作都會產生一個骨架方法和狀態機物件

  • arg - 顯然原始程式碼上有多少個引數,生成的程式碼中就會有多少個欄位

  • __moveNext - 恢復委託函式,對應狀態機中的 MoveNext 方法,該委託函式會在執行過程中作為回撥函式返回給對應Task的 Awaiter 從而使得 MoveNext 持續執行

  • builder - 該結構負責連線狀態機和骨架方法

  • state - 始終從 -1 開始,方法執行時狀態也是1,非負值代表一個後續操作的目標,結束時狀態為 -2

  • Task - 代表當前非同步操作完成後傳播的任務,其內包含正確結果

可以看到,每個由 async 關鍵字標記的非同步操作都會產生相應的骨架方法,而狀態機也會在骨架方法中建立並執行。以下是實際的狀態機內部程式碼,讓我們用實際進行包含兩步非同步操作的 CallMethodAsyncStateMachine 做例子。

[CompilerGenerated]
private sealed class CallMethodAsyncStateMachine : IAsyncStateMachine{    public int state;  
 public string arg;  // 代表變數        public AsyncTaskMethodBuilder<int> builder;        // 代表 result    private int result;        // 代表 var result = await MethodAsync(arg);    private Task<int> firstTaskToAwait;          // 代表 await Task.Delay(result);    private Task secondTaskToAwait;    private void MoveNext()    {    
        try        {          
   switch (this.state) // 初始值為-1            {        
          case -1:                    // 執行 await MethodAsync(arg);                    this.firstTaskToAwait = AsyncMethods.MethodAsync(this.arg);                                        // 當 firstTaskToAwait 執行完畢                    this.result = firstTaskToAwait.Result;                    this.state = 0;                                        // 呼叫 this.MoveNext();                    this.Builder.AwaitUnsafeOnCompleted(ref this.awaiter, ref this);              
           case 0:                
              // 執行 Task.Delay(result)                    this.secondTaskToAwait = Task.Delay(this.result);                                        // 當 secondTaskToAwait 執行完畢                    this.state = 1;                                        // 呼叫 this.MoveNext();                    this.builder.AwaitUnsafeOnCompleted(ref this.awaiter, ref this);            
                  case 1:                    this.builder.SetResult(result);        
                     return;            }        }      
            catch (Exception exception)        {            this.state = -2;            this.builder.SetException(exception);        
                return;        }    }    [DebuggerHidden]  
    private void SetStateMachine(IAsyncStateMachine stateMachine)    {    } }

可以看到一個非同步方法內含有幾個非同步方法,狀態機便會存在幾種分支判斷情況。根據每個分支的執行情況,再通過呼叫 MoveNext 方法確保所有的非同步方法能夠完整執行。更進一步,看似是 switch 和 case 組成的分支方法,實質上仍然是一條非同步操作執行和傳遞的Chain。

上述的 CallMethodAsync 方法也可以轉化成以下 Task.ContinueWith 形式:

internal static async Task<int> CallMethodAsync(string arg){ 
   var result = await (                
      await MethodAsync(arg).ContinueWith(async MethodAsyncTask =>                        {                    
              var methodAsyncTaskResult = await MethodAsyncTask;                            Console.Write(methodAsyncTaskResult);                            await Task.Delay(methodAsyncTaskResult);                            return methodAsyncTaskResult;                        }));  
               return result; }

可以這樣理解的是,總體看來,編譯器每次遇到 await,當前執行的方法都會將方法的剩餘部分註冊為回撥函式(當前 await 任務完成後接下來要進行的工作,也可能包含 await 任務,仍然可以順序巢狀),然後立即返回(return builder.Task)。 剩餘的每個任務將以某種方式完成其操作(可能被排程到當前執行緒上作為事件執行,或者因為使用了 I/O 執行緒執行,或者在單獨執行緒上繼續執行,這其實並不重要),只有在前一個 await 任務標記完成的情況下,才能繼續進行下一個 await 任務。有關這方面的奇思妙想,請參閱《通過 Await 暫停和播放》

.NET Core 2.1 效能提升

上節關於編譯器生成的內容並不能完全涵蓋 async/await 的所有實現概念,甚至只是其中的一小部分,比如筆者並沒有提到可等待模式(IAwaitable)和執行上下文(ExecutionContext)的內容,前者是 async/await 實現的指導原則,後者則是實際執行非同步程式碼,返回給呼叫者結果和執行緒同步的操控者。包括生成的狀態機程式碼中,當第一次執行發現任務並未完成時(!awaiter.isCompleted),任務將直接返回。

主要原因便是這些內容講起來怕是要花很大的篇幅,有興趣的同學推薦去看《深入理解C#》和 ExecutionContext。

非同步程式碼能夠顯著提高伺服器的響應和吞吐效能。但是通過上述講解,想必大家已經認識到為了實現非同步操作,編譯器要自動生成大量的骨架方法和狀態機程式碼,應用通常也要分配更多的相關操作物件,執行緒排程同步也是耗時耗力,這也意味著非同步操作執行效能通常要比同步程式碼要差(這和第一句的效能提升並不矛盾,體重更大的人可能速度降低了,但是抗擊打能力也更強了)。

但是框架開發者一直在為這方面的提升作者努力,最新的 .NET Core 2.1 版本中也提到了這點。原本的應用中,一個基於 async/await 操作的任務將分配以下四個物件:

  1. 返回給呼叫方的Task
    任務實際完成時,呼叫方可以知道任務的返回值等資訊

  2. 裝箱到堆上的狀態機資訊
    之前的程式碼中,我們用了ref標識一開始時,狀態機實際以結構的形式儲存在棧上,但是不可避免的,狀態機執行時,需要被裝箱到堆上以保留一些執行狀態

  3. 傳遞給Awaiter的委託
    即前文的_moveNext,當鏈中的一個 Task 完成時,該委託被傳遞到下一個 Awaiter 執行 MoveNext 方法。

  4. 儲存某些上下文(如ExecutionContext)資訊的狀態機執行者(MoveNextRunner)

據 Performance Improvements in .NET Core 2.1 一文介紹:

for (int i = 0; i < 1000; i++)
{   
 await Yield();  
  async Task Yield() => await Task.Yield(); }

當前的應用將分配下圖中的物件:

640?wx_fmt=png

而在 .NET Core 2.1 中,最終的分配物件將只有:

640?wx_fmt=png

四個分配物件最終減少到一個,分配空間也縮減到了過去的一半。更多的實現資訊可以參考 Avoid async method delegate allocation。

結語

本文主要介紹了 async/await 的實現和 .NET Core 2.1 中關於非同步操作效能優化的相關內容。因為筆者水平一般,文章篇幅有限,不能盡善盡美地解釋完整,還希望大家見諒。

無論是在什麼平臺上,非同步操作都是重要的組成部分,而筆者覺得任何開發者在會用之餘,都應該更進一步地適當瞭解背後的故事。具體發展中,C# 借鑑了 F#中的非同步實現,其他語言諸如 js 可能也借鑑了 C# 中的部分內容,當然一些基本術語,比如回撥或是 feature,任何地方都是相似的,怎麼都脫離不開計算機體系,這也說明了程式設計基礎的重要性。

參考文獻

  1. 通過 Await 暫停和播放

  2. 通過新的 Visual Studio Async CTP 更輕鬆地進行非同步程式設計

原文地址: https://www.cnblogs.com/Wddpct/p/9002242.html

.NET社群新聞,深度好文,歡迎訪問公眾號文章彙總 http://www.csharpkit.com

640?wx_fmt=jpeg