async/await使用深入詳解

gamebus發表於2021-09-09

async和await作為非同步模型程式碼編寫的語法糖已經提供了一段時間不過一直沒怎麼用,由於最近需要在BeetleX webapi中整合對Task方法的支援,所以對async和await有了深入的瞭解和實踐應用.在這總結一下async和await的使用,主要涉及到:自定義Awaitable,在傳統非同步方法中整合Task,異常處理等.

介紹

在傳統非同步方法處理都是透過指定回撥函式的方式來進行處理,這樣對於業務整非常不方便.畢竟業務資訊和狀態往往涉及到多個非同步回撥,這樣業務實現和除錯成本都非常高.為了解決這一問題dotnet推出了async和await語法糖,該語法可以把編寫的程式碼編譯成狀態機模式,從而讓開發員以同步的程式碼方式實現非同步功能的應用.

應用

async和await的使用非常簡單,只需要在方法前加上async關鍵字,然後await所有返回值為Task或ValueTask的方法即可.大概應用如下:

圖片描述

        async void AccessTheWebAsync()
        {            var client = new HttpClient();            var result = await client.GetStringAsync("");
            Console.WriteLine(result);
        }

圖片描述

以上是HttpClient的一個簡單應用,它和傳統的同步呼叫有什麼不同呢?如果用同步GetString那執行緒回等待網路請求完成後再進行輸出,這樣會導致執行緒資源一直浪費在那裡.使用await後,當執行緒執行GetStringAsync後就會釋放出來,然後由網路回撥執行緒來觸發後面的程式碼執行.當然還有一種情況就是GetStringAsync同步完成了當執行緒就會馬上執行Console.WriteLine(result);其實不管那一種情況下都不會讓執行緒等待在那裡浪費資源.

自定義Awaitable

一般情況下async和await都是結合Task來使用,因此可能有人感覺async和await是因Task而存在的;其實async和await是一個語法糖,透過它和相應的程式碼規則來讓編譯器知道怎樣做,但這個規則並不是Task;正確的來說Task是這規則的一種實現,然後應用在大量的方法上,所以自然就使用起來就最普遍了.如果感覺Task太繁瑣使用起來比較重的情況下是完全可以自己實現這個規則,這一規則實現起來也很簡單隻需要簡單地實現一個介面和定義一些方法即可:

    public interface INotifyCompletion
    {        void OnCompleted(Action continuation);
    }

看上去是不是很簡單,不過除了實現這一介面外,還需要定義一些固定名稱的方法

圖片描述

    public interface IAwaitCompletion : INotifyCompletion
    {        bool IsCompleted { get; }        void Success(object data);        void Error(Exception error);

    }    public interface IAwaitObject : IAwaitCompletion
    {

        IAwaitObject GetAwaiter();        object GetResult();

    }

圖片描述

在基礎上再定義一下些行為就可以了,以上IAwaitObject就是實現一個Awaitable所需要的基礎方法行為.不過Success和'Error'方法不是必需要.只是透過這些方法可以讓外部來觸發OnCompleted行為而已. 圍繞介面實現Awaitable的方式也可以根據實際情況應用有所不同,只要需要確保基礎規則實現即可,以下是針對SocketAsyncEventArgs實現的Awaitable

圖片描述

    public class SocketAwaitableEventArgs : SocketAsyncEventArgs, ICriticalNotifyCompletion
    {        private static readonly Action _callbackCompleted = () => { };        private readonly PipeScheduler _ioScheduler;        private Action _callback;        public SocketAwaitableEventArgs(PipeScheduler ioScheduler)
        {
            _ioScheduler = ioScheduler;
        }        public SocketAwaitableEventArgs GetAwaiter() => this;        public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted);        public int GetResult()
        {
            Debug.Assert(ReferenceEquals(_callback, _callbackCompleted));

            _callback = null;            if (SocketError != SocketError.Success)
            {
                ThrowSocketException(SocketError);
            }            return BytesTransferred;            void ThrowSocketException(SocketError e)
            {                throw new SocketException((int)e);
            }
        }        public void OnCompleted(Action continuation)
        {            if (ReferenceEquals(_callback, _callbackCompleted) ||
                ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted))
            {
                Task.Run(continuation);
            }
        }        public void UnsafeOnCompleted(Action continuation)
        {
            OnCompleted(continuation);
        }        public void Complete()
        {
            OnCompleted(this);
        }        protected override void OnCompleted(SocketAsyncEventArgs _)
        {            var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);            if (continuation != null)
            {
                _ioScheduler.Schedule(state => ((Action)state)(), continuation);
            }
        }
    }

圖片描述

以上是Kestrel內部實現的一個Awaitable,它的好處就是可以自己不停地複用,並不需要每次await都要構建一個Task物件.這樣對於大量處理的情況下可以降低物件的開銷減輕GC的負擔來提高效能.

傳統非同步下實現async/await

其實自定義Awaitable就是一種傳統非同步使用async/await功能的一種實現,但對於普通開發人員來說對於狀態不好控制的情況那實現這個Awaitable多多少少有些困難,畢竟還需要大量的測試工作來驗證.其實dotnet已經提供TaskCompletionSource<T>物件來方便應用開發者在傳統非同步下簡單實現async/await.這個物件使用起來也非常方便

圖片描述

        public Task<Response> Execute()
        {
            TaskCompletionSource<Response> taskCompletionSource = new TaskCompletionSource<Response>();
            OnExecute(taskCompletionSource);            return taskCompletionSource.Task;
        }

圖片描述

構建一個TaskCompletionSource<T>物件返回對應的Task即可,然後在非同步完成的地方呼叫相關方法即可簡單實現傳統非同步支援async/await

taskCompletionSource.TrySetResult(response)

taskCompletionSource.TrySetError(exception)

在這裡不得不說一下TaskCompletionSource<T>的設計,非要加個泛型.如果結合反射使用就有點蛋碎了,畢竟這個方法並不提供object設定,除非上層定義TaskCompletionSource<Object>但這樣定義就失去了T的意義了....還好這個類可繼承的給使用者留了一個後路.以下做了簡單的封裝讓它支援object返回值傳入

圖片描述

    interface IAnyCompletionSource
    {        void Success(object data);        void Error(Exception error);        void WaitResponse(Task<Response> task);
        Task GetTask();
    }    class AnyCompletionSource<T> : TaskCompletionSource<T>, IAnyCompletionSource
    {        public void Success(object data)
        {
            TrySetResult((T)data);
        }        public void Error(Exception error)
        {
            TrySetException(error);
        }        public async void WaitResponse(Task<Response> task)
        {            var response = await task;            if (response.Exception != null)
                Error(response.Exception);            else
                Success(response.Body);
        }        public Task GetTask()
        {            return this.Task;
        }
    }

圖片描述

異常處理

由於async/await最終編譯成狀態機程式碼,所以異常處理會和普通程式碼不同,一連串的async/await方法裡,一般只需要在最頂的斷層方法Try即可,一般這個斷層的方法是async void,或Task.wait處;和傳統方法異常處理不一樣,如果再往上一層是無法Try住這些異常的,當現現這情況的時候往往就是未知異常導致程式死掉.以下是一個錯誤的處理程式碼:

圖片描述

        static void Main(string[] args)
        {            try
            {
                Test();
            }            catch (Exception e_)
            {
                Console.WriteLine(e_);
            }
            Console.Read();
        }        static async void Test()
        {
            Console.WriteLine(await PrintValue());
        }        static async Task<bool> PrintValue()
        {            var value = await GetUrl();
            Console.WriteLine(value);            return true;
        }        static async Task<string> GetUrl()
        {            var client = new HttpClient();            return await client.GetStringAsync("asd");
        }

圖片描述

正確有效的Try地方是在Test方法裡

圖片描述

        static async void Test()
        {            try
            {
                Console.WriteLine(await PrintValue());
            }            catch (Exception e_)
            {
                Console.WriteLine(e_);
            }
        }        static async Task<bool> PrintValue()
        {            var value = await GetUrl();
            Console.WriteLine(value);            return true;
        }        static async Task<string> GetUrl()
        {            var client = new HttpClient();            return await client.GetStringAsync("asd");
        }

圖片描述

一些注意事項和技巧

  1. 自定義async/await時候,預設都是由非同步完成執行緒來觸發狀態機,但這裡存在一個風險當這個觸發狀態機的程式碼是在鎖範圍內執行就需要特別小心,很多時候再次迴歸執行獲取鎖的時候就導致無法得到引起程式碼無法執行的問題.

  2. 在使用的await之前其實是可以先判斷一下完成狀態,如果是完成就沒有必然引用await來處理狀態機的工作,這樣一定程度降低狀態的執行和開銷.

  3. 如果你的方法可以是同步完成,如一些記憶體操作那最好用ValueTask代替Task

  4. 其實反射裡使用async/await也是非常方便的,只需要判斷一下物件是否Awaitable,如果是就執行await處理狀態機.

原文出處: https://www.cnblogs.com/smark/p/10159796.html  

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4687/viewspace-2819119/,如需轉載,請註明出處,否則將追究法律責任。

相關文章