溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

C#異步編程之a(chǎn)sync/await怎么使用

發(fā)布時(shí)間:2023-03-11 09:41:59 來(lái)源:億速云 閱讀:171 作者:iii 欄目:開(kāi)發(fā)技術(shù)

今天小編給大家分享一下C#異步編程之a(chǎn)sync/await怎么使用的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來(lái)了解一下吧。

概述

異步這個(gè)概念在不同語(yǔ)境下有不同的解釋,比如在一個(gè)單核CPU里開(kāi)啟兩個(gè)線程執(zhí)行兩個(gè)函數(shù),通常認(rèn)為這種調(diào)用是異步的,但對(duì)于CPU來(lái)說(shuō)它是單核不可能同時(shí)運(yùn)行兩個(gè)函數(shù),不過(guò)是由系統(tǒng)調(diào)度在不同的時(shí)間分片中執(zhí)行。一般來(lái)說(shuō),如果兩個(gè)工作能同時(shí)進(jìn)行,就認(rèn)為是異步的。在編程中,它通常代表函數(shù)的調(diào)用可以在不執(zhí)行完的情況下返回,必要時(shí)在完成時(shí)回調(diào)。

有一個(gè)概念常常被混淆,多線程和異步。很多人認(rèn)為異步就是多線程的,但是多線程只是實(shí)現(xiàn)異步的其中一種方式,除此之外還有系統(tǒng)中斷,定時(shí)器,甚至可以自己寫(xiě)一個(gè)狀態(tài)機(jī)實(shí)現(xiàn)異步(C# 的異步實(shí)現(xiàn)類似狀態(tài)機(jī))。

不同的編程語(yǔ)言有不同異步編程方法,在C#語(yǔ)言中,常常使用async/await等關(guān)鍵字,和Task等類來(lái)實(shí)現(xiàn)異步編程。

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函數(shù)異步調(diào)用IntTask,打印"等三秒吧",隨后返回到Main函數(shù)打印“等待中”,在task.Result取值時(shí)阻塞,三秒后IntTask返回(此時(shí)Task.Result被賦值)打印“result = 1”??匆幌掠梅ǎ?/p>

  • async: 異步函數(shù)使用async關(guān)鍵字修飾

  • await: 等待異步函數(shù)返回

  • Task:異步函數(shù)有返回值,且返回值為int類型

上述只是一個(gè)極簡(jiǎn)的用法,忽略了大量的細(xì)節(jié),可以建立一個(gè)初步的印象。

async/await和Task簡(jiǎn)介

async

用async修飾一個(gè)方法,表明這個(gè)方法可以異步執(zhí)行,其返回值必須是void/Task/Task<T>(T是返回值類型)其中一個(gè),方法內(nèi)的語(yǔ)句至少包含一個(gè)await關(guān)鍵字,否則會(huì)被同步的方式執(zhí)行。

await

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

Task

源于基于任務(wù)的異步模式(Task-based Asynchronous Pattern,TAP),被作為異步函數(shù)的返回值。異步函數(shù)的返回值有三種:

  • void:"fire and forget"(觸發(fā)并忘記)不需要知道狀態(tài)(是否完成),比如拋出異常、打印日志時(shí)可以使用

  • Task:需要知道是否完成(或失敗)等狀態(tài),但是不需要返回值

  • Task<T>:在Task的基礎(chǔ)上還想要返回值

其他

異步函數(shù)不能使用ref/out修飾參數(shù)

實(shí)現(xiàn)原理剖析

如果使用反匯編等手段,可以看到上述示例代碼的編譯:

C#異步編程之a(chǎn)sync/await怎么使用

在返回1之前,好像有什么“奇怪的東西”被調(diào)用,編譯器又背著開(kāi)發(fā)者偷偷干了什么呢?

實(shí)現(xiàn)原理示例

在微軟的開(kāi)發(fā)博客里有一個(gè)叫謝爾蓋&middot;杰普利亞科夫(Sergey Tepliakov)的毛子曾提到這部分,來(lái)看一下他的示例:

源碼

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

這是他的源代碼,這個(gè)類叫做StockPrices(股票價(jià)格),其核心業(yè)務(wù)是根據(jù)公司ID查詢股票價(jià)格GetStockPriceForAsync,這是一個(gè)異步調(diào)用,首先它先異步調(diào)用InitializeMapIfNeededAsync對(duì)數(shù)據(jù)庫(kù)進(jìn)行初始化,初始化完成嘗試從數(shù)據(jù)庫(kù)中獲取該公司的股票價(jià)格返回。

上述提到編譯器偷偷自己生成了代碼,如果手動(dòng)實(shí)現(xiàn)大概是怎樣的呢?來(lái)看謝爾蓋給出的解:

手動(dòng)實(shí)現(xiàn)

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可以看到,他為這個(gè)異步調(diào)用生成了一個(gè)狀態(tài)機(jī)來(lái)實(shí)現(xiàn)異步,先來(lái)看下成員變量:

  • StockPrices: 原來(lái)那個(gè)“股票價(jià)格”類的引用

  • _companyId: 調(diào)用方法時(shí)的參數(shù)公司ID

  • _tcs:TaskCompletionSource 創(chuàng)建并完成該任務(wù)的來(lái)源。

  • _initializeMapIfNeededTask:調(diào)用初始化數(shù)據(jù)的異步任務(wù)

  • _state:狀態(tài)枚舉

  • Task:直接就是_tcs.Task,即該任務(wù)創(chuàng)建并完成的來(lái)源

現(xiàn)在看來(lái)這段代碼的邏輯就比較清楚了,在調(diào)用異步查詢股票的接口時(shí),創(chuàng)建了一個(gè)狀態(tài)機(jī)并調(diào)用狀態(tài)機(jī)的Start函數(shù),第一次進(jìn)入start函數(shù)時(shí)狀態(tài)機(jī)的狀態(tài)是Start狀態(tài),它給_initializeMapIfNeededTask賦值,把狀態(tài)機(jī)狀態(tài)流轉(zhuǎn)到Step1,并讓_initializeMapIfNeededTask執(zhí)行結(jié)束末尾再次調(diào)用Start函數(shù)(ContinueWith)。

_initializeMapIfNeededTask任務(wù)在等待了42毫秒后(Task.Delay(42)),末尾時(shí)再次調(diào)用了Start函數(shù),此時(shí)狀態(tài)為Step1。首先檢查了Task狀態(tài),符合要求調(diào)用_tcs.SetResult(其實(shí)是給Task的Result賦值),此時(shí)異步任務(wù)完成。

TaskCompletionSource

看官方文檔給的定義:

表示未綁定到委托的 Task<TResult> 的制造者方,并通過(guò) Task 屬性提供對(duì)使用者方的訪問(wèn)

簡(jiǎn)單的示例:

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

看的出來(lái)這個(gè)類就是對(duì)Task的包裝,方便創(chuàng)建分發(fā)給使用者的任務(wù)。其核心就是包裝Task并方便外面設(shè)置其屬性和狀態(tài)

Task.ContinueWith

創(chuàng)建一個(gè)在目標(biāo) Task 完成時(shí)異步執(zhí)行的延續(xù)任務(wù)

可以傳入一個(gè)委托,在Task完成的末尾調(diào)用。這是一個(gè)典型的續(xù)體傳遞風(fēng)格(continuation-pass style)。

續(xù)體傳遞風(fēng)格

續(xù)體傳遞風(fēng)格(continuation-pass style, CPS),來(lái)看維基百科的描述:

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

大概的意思是,這種風(fēng)格的函數(shù)比起普通的有一個(gè)額外的函數(shù)指針參數(shù),調(diào)用結(jié)束(即將return)調(diào)用或者函數(shù)參數(shù)(替代直接return到調(diào)用者Caller)。還有一些其他細(xì)節(jié),就不多說(shuō)了,感興趣自行翻譯查看。

來(lái)看一個(gè)極簡(jiǎn)的例子:

int a = b + c + d;

這是一個(gè)鏈?zhǔn)竭\(yùn)算,是有順序的,在C++中,上述運(yùn)算其實(shí)是:

int a = (b + c) + d; 先計(jì)算tmp = b + c(tmp是寄存器上一個(gè)臨時(shí)的值,也稱將亡值),然后計(jì)算 int a = tmp + c

使用續(xù)體傳遞來(lái)模擬這一過(guò)程:

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

上述代碼應(yīng)該很清楚了,稍微看下應(yīng)該能看明白。

C#編譯器的實(shí)現(xiàn)

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#編譯器生成的狀態(tài)機(jī):

  • __this:StockPrices“股票價(jià)格”類的引用

  • companyId:公司ID參數(shù)

  • __builder:AsyncTaskMethodBuilder類型的表示返回任務(wù)的異步方法生成器

  • __state:狀態(tài)索引

  • __task1Awaiter:TaskAwaiter類型,提供等待異步任務(wù)完成的對(duì)象

上述成員有一些和之前手?jǐn)]的狀態(tài)機(jī)不太一樣,等下面會(huì)介紹,先來(lái)這一套的邏輯:
首先創(chuàng)建了一個(gè)_GetStockPriceForAsync_d__1狀態(tài)機(jī)_GetStockPriceFor_d__并初始化賦值,隨后調(diào)用了這個(gè)狀態(tài)機(jī)的__builder的Start函數(shù)并把該狀態(tài)機(jī)作為引用參數(shù)傳入。__builder.Start函數(shù)會(huì)調(diào)到該狀態(tài)機(jī)的MoveNext函數(shù)(下面會(huì)介紹),這和手?jǐn)]代碼狀態(tài)機(jī)Start函數(shù)調(diào)用類似。

MoveNext與Start函數(shù)的處理過(guò)程也類似:第一次進(jìn)來(lái)__state == -1,__builder.AwaitUnsafeOnCompleted切換上下文執(zhí)行InitializeLocalStoreIfNeededAsync異步任務(wù),并指定在完成后切換到當(dāng)前上下文調(diào)用該狀態(tài)機(jī)的MoveNext函數(shù),類似手?jǐn)]代碼的Task.ContinueWith。第二次進(jìn)入時(shí),執(zhí)行到__builder.SetResult(result),異步任務(wù)基本完成。

上述描述也是忽略了一些細(xì)節(jié),下面是調(diào)用的時(shí)序圖,會(huì)更清楚些,有些不太清楚的點(diǎn)后面會(huì)詳細(xì)介紹。

C#異步編程之a(chǎn)sync/await怎么使用

TaskAwaiter

來(lái)看下官方定義:

public readonly struct TaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion 提供等待異步任務(wù)完成的對(duì)象

結(jié)構(gòu):

C#異步編程之a(chǎn)sync/await怎么使用

可以看到,這個(gè)所謂“等待異步任務(wù)完成的對(duì)象”,主要是保證實(shí)現(xiàn)ICriticalNotifyCompletion的接口OnCompleted等。

AsyncTaskMethodBuilder<TAwaiter,TStateMachine>(TAwaiter, TStateMachine)

官方定義:

C#異步編程之a(chǎn)sync/await怎么使用

個(gè)人認(rèn)為可以視為異步任務(wù)的“門(mén)面”,它負(fù)責(zé)啟動(dòng)狀態(tài)機(jī),傳遞一些中間狀態(tài),并在最終SetResult時(shí)表示它和其子例程的異步任務(wù)結(jié)束。其中有一個(gè)方法AwaitUnsafeOnCompleted,值得研究一下。

AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted

這個(gè)方法在上述中一筆帶過(guò),被描述為類似Task.ContinueWith,確實(shí)如此,但執(zhí)行過(guò)程相當(dāng)復(fù)雜,在這里也只是簡(jiǎn)單介紹下過(guò)程。

AwaitUnsafeOnCompleted首先會(huì)調(diào)用GetCompletionAction,GetCompletionAction創(chuàng)建了一個(gè)保存了上下文 context = ExecuteContext.FastCapture()的MoveNextRunner,并返回了指向的MoveNextRunner.Run函數(shù)的委托。

接著調(diào)用參數(shù)awaiter的UnsafeOnCompleted(completionAction)函數(shù),這里completionAction就是上述的那個(gè)委托,內(nèi)部調(diào)用了成員Task.SetContinuationForAwait函數(shù)來(lái)初始化續(xù)體,SetContinuationForAwait又調(diào)用AddTaskContinuation把延續(xù)方法添加到Task中,當(dāng)上述示例源碼中的InitializeMapIfNeededAsync函數(shù)執(zhí)行完調(diào)用Runner.Run:

[SecuritySafeCritical]
internal void Run()
{
    if (this.m_context != null)
    {
        try
        {
            // 我們并未給 s_invokeMoveNext 賦值,所以 callback == null
            ContextCallback callback = s_invokeMoveNext;
            if (callback == null)
            {
                // 將回調(diào)設(shè)置為下方的 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() 重新調(diào)用了狀態(tài)機(jī)的MoveNext()。

以上就是“C#異步編程之a(chǎn)sync/await怎么使用”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI