C# 非同步編程async/await

来源:https://www.cnblogs.com/hggzhang/archive/2023/03/10/17199017.html
-Advertisement-
Play Games

概述 非同步這個概念在不同語境下有不同的解釋,比如在一個單核CPU里開啟兩個線程執行兩個函數,通常認為這種調用是非同步的,但對於CPU來說它是單核不可能同時運行兩個函數,不過是由系統調度在不同的時間分片中執行。一般來說,如果兩個工作能同時進行,就認為是非同步的。在編程中,它通常代表函數的調用可以在不執行完 ...


概述

非同步這個概念在不同語境下有不同的解釋,比如在一個單核CPU里開啟兩個線程執行兩個函數,通常認為這種調用是非同步的,但對於CPU來說它是單核不可能同時運行兩個函數,不過是由系統調度在不同的時間分片中執行。一般來說,如果兩個工作能同時進行,就認為是非同步的。在編程中,它通常代表函數的調用可以在不執行完的情況下返回,必要時在完成時回調。

有一個概念常常被混淆,多線程和非同步。很多人認為非同步就是多線程的,但是多線程只是實現非同步的其中一種方式,除此之外還有系統中斷,定時器,甚至可以自己寫一個狀態機實現非同步(C# 的非同步實現類似狀態機)。

不同的編程語言有不同非同步編程方法,在C#語言中,常常使用async/await等關鍵字,和Task等類來實現非同步編程。

C#非同步編程用法

class Program
{
    static void Main(string[] args)
    {
        var task =  IntTask();
		Console.WriteLine("等待中...");
        Console.WriteLine($"算完了? 讓我康康! result = {task.Result}");
    }

    static async Task<int> IntTask()
    {
        Console.WriteLine("等3秒吧");
        await Task.Delay(3000);
        return 1;
    }
}

Main函數非同步調用IntTask,列印"等三秒吧",隨後返回到Main函數列印“等待中”,在task.Result取值時阻塞,三秒後IntTask返回(此時Task.Result被賦值)列印“result = 1”。看一下用法:

  • async: 非同步函數使用async關鍵字修飾
  • await: 等待非同步函數返回
  • Task:非同步函數有返回值,且返回值為int類型

上述只是一個極簡的用法,忽略了大量的細節,可以建立一個初步的印象。

async/await和Task簡介

async

用async修飾一個方法,表明這個方法可以非同步執行,其返回值必須是void/Task/Task<T>(T是返回值類型)其中一個,方法內的語句至少包含一個await關鍵字,否則會被同步的方式執行。

await

await只能修飾(返回值是)Task類型變數,此時會返回Task.Result或void而不是Task本身,在上述示例中,Main沒有被async修飾,不能使用await,其返回值就是Task<int>, 而IntTask調用Task.Delay就是直接返回void。await也只能在被async修飾的函數的語句中使用。

Task

源於基於任務的非同步模式(Task-based Asynchronous Pattern,TAP),被作為非同步函數的返回值。非同步函數的返回值有三種:

  • void:"fire and forget"(觸發並忘記)不需要知道狀態(是否完成),比如拋出異常、列印日誌時可以使用
  • Task:需要知道是否完成(或失敗)等狀態,但是不需要返回值
  • Task<T>:在Task的基礎上還想要返回值

其他

  • 非同步函數不能使用ref/out修飾參數

實現原理剖析

如果使用反彙編等手段,可以看到上述示例代碼的編譯:
image
在返回1之前,好像有什麼“奇怪的東西”被調用,編譯器又背著開發者偷偷幹了什麼呢?

實現原理示例

在微軟的開發博客里有一個叫謝爾蓋·傑普利亞科夫(Sergey Tepliakov)的毛子曾提到這部分,來看一下他的示例:

源碼

class StockPrices
{
    private Dictionary<string, decimal> _stockPrices;
    public async Task<decimal> GetStockPriceForAsync(string companyId)
    {
        await InitializeMapIfNeededAsync();
        _stockPrices.TryGetValue(companyId, out var result);
        return result;
    }
 
    private async Task InitializeMapIfNeededAsync()
    {
        if (_stockPrices != null)
            return;
 
        await Task.Delay(42);
        // Getting the stock prices from the external source and cache in memory.
        _stockPrices = new Dictionary<string, decimal> { { "MSFT", 42 } };
    }
}

這是他的源代碼,這個類叫做StockPrices(股票價格),其核心業務是根據公司ID查詢股票價格GetStockPriceForAsync,這是一個非同步調用,首先它先非同步調用InitializeMapIfNeededAsync對資料庫進行初始化,初始化完成嘗試從資料庫中獲取該公司的股票價格返回。
上述提到編譯器偷偷自己生成了代碼,如果手動實現大概是怎樣的呢?來看謝爾蓋給出的解:

手動實現

class GetStockPriceForAsync_StateMachine
{
    enum State { Start, Step1, }
    private readonly StockPrices @this;
    private readonly string _companyId;
    private readonly TaskCompletionSource<decimal> _tcs;
    private Task _initializeMapIfNeededTask;
    private State _state = State.Start;

    public GetStockPriceForAsync_StateMachine(StockPrices @this, string companyId)
    {
        this.@this = @this;
        _companyId = companyId;
    }

    public void Start()
    {
        try
        {
            if (_state == State.Start)
            {
                // The code from the start of the method to the first 'await'.

                if (string.IsNullOrEmpty(_companyId))
                    throw new ArgumentNullException();

                _initializeMapIfNeededTask = @this.InitializeMapIfNeeded();

                // Update state and schedule continuation
                _state = State.Step1;
                _initializeMapIfNeededTask.ContinueWith(_ => Start());
            }
            else if (_state == State.Step1)
            {
                // Need to check the error and the cancel case first
                if (_initializeMapIfNeededTask.Status == TaskStatus.Canceled)
                    _tcs.SetCanceled();
                else if (_initializeMapIfNeededTask.Status == TaskStatus.Faulted)
                    _tcs.SetException(_initializeMapIfNeededTask.Exception.InnerException);
                else
                {
                    // The code between first await and the rest of the method

                    @this._store.TryGetValue(_companyId, out var result);
                    _tcs.SetResult(result);
                }
            }
        }
        catch (Exception e)
        {
            _tcs.SetException(e);
        }
    }

    public Task<decimal> Task => _tcs.Task;
}

public Task<decimal> GetStockPriceForAsync(string companyId)
{
    var stateMachine = new GetStockPriceForAsync_StateMachine(this, companyId);
    stateMachine.Start();
    return stateMachine.Task;
}

從類名GetStockPriceForAsync_StateMachine可以看到,他為這個非同步調用生成了一個狀態機來實現非同步,先來看下成員變數:

  • StockPrices: 原來那個“股票價格”類的引用
  • _companyId: 調用方法時的參數公司ID
  • _tcs:TaskCompletionSource 創建並完成該任務的來源。
  • _initializeMapIfNeededTask:調用初始化數據的非同步任務
  • _state:狀態枚舉
  • Task:直接就是_tcs.Task,即該任務創建並完成的來源

現在看來這段代碼的邏輯就比較清楚了,在調用非同步查詢股票的介面時,創建了一個狀態機並調用狀態機的Start函數,第一次進入start函數時狀態機的狀態是Start狀態,它給_initializeMapIfNeededTask賦值,把狀態機狀態流轉到Step1,並讓_initializeMapIfNeededTask執行結束末尾再次調用Start函數(ContinueWith)。

_initializeMapIfNeededTask任務在等待了42毫秒後(Task.Delay(42)),末尾時再次調用了Start函數,此時狀態為Step1。首先檢查了Task狀態,符合要求調用_tcs.SetResult(其實是給Task的Result賦值),此時非同步任務完成。

TaskCompletionSource

看官方文檔給的定義:

表示未綁定到委托的 Task<TResult> 的製造者方,並通過 Task 屬性提供對使用者方的訪問

簡單的示例:

static void Main()
{
	TaskCompletionSource<int> tcs1 = new TaskCompletionSource<int>();
	Task<int> t1 = tcs1.Task;

	// Start a background task that will complete tcs1.Task
	Task.Factory.StartNew(() =>
	{
		Thread.Sleep(1000);
		tcs1.SetResult(15);
	});
}

看的出來這個類就是對Task的包裝,方便創建分發給使用者的任務。其核心就是包裝Task並方便外面設置其屬性和狀態

Task.ContinueWith

創建一個在目標 Task 完成時非同步執行的延續任務

可以傳入一個委托,在Task完成的末尾調用。這是一個典型的續體傳遞風格(continuation-pass style)。

續體傳遞風格

續體傳遞風格(continuation-pass style, CPS),來看維基百科的描述:

A function written in continuation-passing style takes an extra argument: an explicit "continuation"; i.e., a function of one argument. When the CPS function has computed its result value, it "returns" it by calling the continuation function with this value as the argument. That means that when invoking a CPS function, the calling function is required to supply a procedure to be invoked with the subroutine's "return" value. Expressing code in this form makes a number of things explicit which are implicit in direct style. These include: procedure returns, which become apparent as calls to a continuation; intermediate values, which are all given names; order of argument evaluation, which is made explicit; and tail calls, which simply call a procedure with the same continuation, unmodified, that was passed to the caller

大概的意思是,這種風格的函數比起普通的有一個額外的函數指針參數,調用結束(即將return)調用或者函數參數(替代直接return到調用者Caller)。還有一些其他細節,就不多說了,感興趣自行翻譯查看。

來看一個極簡的例子:
int a = b + c + d;
這是一個鏈式運算,是有順序的,在C++中,上述運算其實是:
int a = (b + c) + d; 先計算tmp = b + c(tmp是寄存器上一個臨時的值,也稱將亡值),然後計算 int a = tmp + c
使用續體傳遞來模擬這一過程:

class Program
{
    public class Result<T>
    {
        public T V;
    }

    static void Main(string[] args)
    {
        int a = 1;
        int b = 2;
        int c = 3;
        int d = 4;

        Result<int> ar = new() { V = a};
        Calc3(ar, b, c, d, Calc2);

        Console.WriteLine($"a = {ar.V}");
    }

    static void Calc3(Result<int> ar, int b, int c, int d, Action<Result<int>, int, int> continues)
    {
        int tmp = b + c;
        continues(ar, tmp, d);
    }

    static void Calc2(Result<int> ar, int tmp, int d)
    {
        ar.V = tmp + d;
    }
}

上述代碼應該很清楚了,稍微看下應該能看明白。

C#編譯器的實現

struct _GetStockPriceForAsync_d__1 : IAsyncStateMachine
{
    public StockPrices __this;
    public string companyId;
    public AsyncTaskMethodBuilder<decimal> __builder;
    public int __state;
    private TaskAwaiter __task1Awaiter;

    public void MoveNext()
    {
        decimal result;
        try
        {
            TaskAwaiter awaiter;
            if (__state != 0)
            {
                // State 1 of the generated state machine:
                if (string.IsNullOrEmpty(companyId))
                    throw new ArgumentNullException();

                awaiter = __this.InitializeLocalStoreIfNeededAsync().GetAwaiter();

                // Hot path optimization: if the task is completed,
                // the state machine automatically moves to the next step
                if (!awaiter.IsCompleted)
                {
                    __state = 0;
                    __task1Awaiter = awaiter;

                    // The following call will eventually cause boxing of the state machine.
                    __builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
                    return;
                }
            }
            else
            {
                awaiter = __task1Awaiter;
                __task1Awaiter = default(TaskAwaiter);
                __state = -1;
            }

            // GetResult returns void, but it'll throw if the awaited task failed.
            // This exception is catched later and changes the resulting task.
            awaiter.GetResult();
            __this._stocks.TryGetValue(companyId, out result);
        }
        catch (Exception exception)
        {
            // Final state: failure
            __state = -2;
            __builder.SetException(exception);
            return;
        }

        // Final state: success
        __state = -2;
        __builder.SetResult(result);
    }

    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        __builder.SetStateMachine(stateMachine);
    }
}

[AsyncStateMachine(typeof(_GetStockPriceForAsync_d__1))]
public Task<decimal> GetStockPriceFor(string companyId)
{
    _GetStockPriceForAsync_d__1 _GetStockPriceFor_d__;
    _GetStockPriceFor_d__.__this = this;
    _GetStockPriceFor_d__.companyId = companyId;
    _GetStockPriceFor_d__.__builder = AsyncTaskMethodBuilder<decimal>.Create();
    _GetStockPriceFor_d__.__state = -1;
    var __t__builder = _GetStockPriceFor_d__.__builder;
    __t__builder.Start<_GetStockPriceForAsync_d__1>(ref _GetStockPriceFor_d__);
    return _GetStockPriceFor_d__.__builder.Task;
}

比較一下C#編譯器生成的狀態機:

  • __this:StockPrices“股票價格”類的引用
  • companyId:公司ID參數
  • __builder:AsyncTaskMethodBuilder類型的表示返回任務的非同步方法生成器
  • __state:狀態索引
  • __task1Awaiter:TaskAwaiter類型,提供等待非同步任務完成的對象

上述成員有一些和之前手擼的狀態機不太一樣,等下麵會介紹,先來這一套的邏輯:
首先創建了一個_GetStockPriceForAsync_d__1狀態機_GetStockPriceFor_d__並初始化賦值,隨後調用了這個狀態機的__builder的Start函數並把該狀態機作為引用參數傳入。__builder.Start函數會調到該狀態機的MoveNext函數(下麵會介紹),這和手擼代碼狀態機Start函數調用類似。

MoveNext與Start函數的處理過程也類似:第一次進來__state == -1,__builder.AwaitUnsafeOnCompleted切換上下文執行InitializeLocalStoreIfNeededAsync非同步任務,並指定在完成後切換到當前上下文調用該狀態機的MoveNext函數,類似手擼代碼的Task.ContinueWith。第二次進入時,執行到__builder.SetResult(result),非同步任務基本完成。

上述描述也是忽略了一些細節,下麵是調用的時序圖,會更清楚些,有些不太清楚的點後面會詳細介紹。
image

TaskAwaiter

來看下官方定義:

public readonly struct TaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion 提供等待非同步任務完成的對象

結構:
image
可以看到,這個所謂“等待非同步任務完成的對象”,主要是保證實現ICriticalNotifyCompletion的介面OnCompleted等。

AsyncTaskMethodBuilder<TAwaiter,TStateMachine>(TAwaiter, TStateMachine)

官方定義:
image
個人認為可以視為非同步任務的“門面”,它負責啟動狀態機,傳遞一些中間狀態,併在最終SetResult時表示它和其子常式的非同步任務結束。其中有一個方法AwaitUnsafeOnCompleted,值得研究一下。

AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted

這個方法在上述中一筆帶過,被描述為類似Task.ContinueWith,確實如此,但執行過程相當複雜,在這裡也只是簡單介紹下過程。

AwaitUnsafeOnCompleted首先會調用GetCompletionAction,GetCompletionAction創建了一個保存了上下文 context = ExecuteContext.FastCapture()的MoveNextRunner,並返回了指向的MoveNextRunner.Run函數的委托。

接著調用參數awaiter的UnsafeOnCompleted(completionAction)函數,這裡completionAction就是上述的那個委托,內部調用了成員Task.SetContinuationForAwait函數來初始化續體,SetContinuationForAwait又調用AddTaskContinuation把延續方法添加到Task中,當上述示例源碼中的InitializeMapIfNeededAsync函數執行完調用Runner.Run:

[SecuritySafeCritical]
internal void Run()
{
    if (this.m_context != null)
    {
        try
        {
            // 我們並未給 s_invokeMoveNext 賦值,所以 callback == null
            ContextCallback callback = s_invokeMoveNext;
            if (callback == null)
            {
                // 將回調設置為下方的 InvokeMoveNext 方法
                s_invokeMoveNext = callback = new
                ContextCallback(AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext);
            }
            ExecutionContext.Run(this.m_context, callback, this.m_stateMachine, true);
            return;
        }
        finally
        {
            this.m_context.Dispose();
        }
    }
    this.m_stateMachine.MoveNext();
}

[SecurityCritical]
private static void InvokeMoveNext(object stateMachine)
{
    ((IAsyncStateMachine) stateMachine).MoveNext();
}

((IAsyncStateMachine) stateMachine).MoveNext() 重新調用了狀態機的MoveNext()

參考鏈接


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文介紹基於Python中GDAL模塊,實現MODIS遙感影像數據的讀取、計算,並基於質量控制QC波段進行圖像掩膜的方法~ ...
  • 簡介 編程是一門藝術,就像繪畫和音樂一樣,需要創造力和想象力。編程的藝術在於它可以創造出獨特的、精美的、功能強大的程式。 在編程中,結構是一種重要的美學概念。好的結構可以使程式更加清晰、易於理解和維護。結構設計是通過分解問題和組織代碼來創造良好的程式結構的過程。 良好的程式結構需要滿足以下幾個方面: ...
  • 2023Java面試題最經典的問題之一了,非常經典的Java基礎知識,一定要學會! 在Java中,String類被設計成final,這意味著它的值在創建後不可更改。這是因為字元串在Java中使用廣泛,作為文本處理、網路通信等方面的核心數據類型。如果String類是可變的,那麼在使用時可能會出現安全問 ...
  • 最近發現一個不錯的免費開源學習項目:30天學會Python 如果您最近有學習Python的打算,不妨看看這個是否適合你? 項目地址:https://github.com/Asabeneh/30-Days-Of-Python 博客地址:https://blog.didispace.com/tj-30- ...
  • 最新版 IDEA 2022.3.2 中配置熱載入工具 DevTools 在SpringBoot開發調試中,如果我每行代碼的修改都需要重啟再調試,可能比較費時間;SpringBoot團隊針對此問題提供了spring-boot-devtools(簡稱devtools)插件,用此插件提升開發調試的效率。 ...
  • 前言 之前寫過一篇基於ML.NET的手部關鍵點分類的博客,可以根據圖片進行手部的提取分類,於是我就將手勢分類和攝像頭數據結合,集成到了我開發的電子腦殼軟體里。 電子腦殼是一個為稚暉君開源的桌面機器人ElectronBot提供一些軟體功能的桌面程式項目。它是由綠蔭阿廣也就是我開發的,使用了微軟的WAS ...
  • 最近在使用 Blazor 開發管理後臺時遇到瞭如下的問題,我這裡後臺整體採用了 AntDesignBlazor 組件庫,在上線之後發現ReuseTabs組件在使用過程中,如果預設 / 沒有指定為項目的base href,打開標簽頁後,相互切換會導致url錯誤。 本地開發的時候項目是直接啟動運行的,所 ...
  • 在DNF中,角色貼圖以.ani文件的坐標為中心,NPK中png的坐標為繪製坐標(坐上)進行繪製,繪製的結果如圖所示: 原點坐標-232,-333 原點坐標-232,-333 皮膚坐標207,224 皮膚坐標207,224 太刀柄194,264 太刀柄194,264 太刀刃213,283 太刀刃213 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...