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

前言

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

在平常工做中,筆者接觸得最多的開發工做仍然是在 .NET Core 平臺上,固然由於團隊領導的開放性和團隊風格的多樣性(這和 CTO 以及主管的我的能力也是分不開的),業界前沿的技術概念也都能在上手的項目中出現。因此雖然如今團隊仍然處於疾速的發展中,也存在一些奇奇怪怪的事情,工做內容也算有緊有鬆,可是整體來講也算有苦有樂,不是十分排斥。github

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

因此語言之爭也好,平臺之爭也好,落到每個對編程和解決問題感興趣的開發者身上,便成了最微不足道的問題。可以感覺不一樣技術間的碰撞,領略到不一樣架構思想中的精妙,就已是一件知足的事情了,等到團隊須要你快速應用其餘技術選型時,以前的努力也是助力。固然面向工資編程也是一種取捨,筆者思考的時候也會陷入這個怪圈,因此但願在不斷的學習和實踐中,可以讓本身更滿意吧。編程

著名的 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)和狀態機。框架

從反編譯器中咱們能夠窺見骨架方法的全貌。假設有這樣一個示例程序asp.net

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();
}

當前的應用將分配下圖中的對象:

此處輸入圖片的描述

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

此處輸入圖片的描述

四個分配對象最終減小到一個,分配空間也縮減到了過去的一半。更多的實現信息能夠參考 Avoid async method delegate allocation

結語

本文主要介紹了 async/await 的實現和 .NET Core 2.1 中關於異步操做性能優化的相關內容。由於筆者水平通常,文章篇幅有限,不能盡善盡美地解釋完整,還但願你們見諒。

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

參考文獻

  1. 經過 Await 暫停和播放
  2. 經過新的 Visual Studio Async CTP 更輕鬆地進行異步編程
相關文章
相關標籤/搜索