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("https://msdn.microsoft.com"); 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("https://msdn.microsoft.comasd"); }
正確有效的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("https://msdn.microsoft.comasd"); }
一些注意事項和技巧
- 自定義async/await時候,默認都是由異步完成線程來觸發狀態機,但這里存在一個風險當這個觸發狀態機的代碼是在鎖范圍內執行就需要特別小心,很多時候再次回歸執行獲取鎖的時候就導致無法得到引起代碼無法執行的問題.
- 在使用的await之前其實是可以先判斷一下完成狀態,如果是完成就沒有必然引用await來處理狀態機的工作,這樣一定程度降低狀態的執行和開銷.
- 如果你的方法可以是同步完成,如一些內存操作那最好用ValueTask代替Task
- 其實反射里使用async/await也是非常方便的,只需要判斷一下對象是否Awaitable,如果是就執行await處理狀態機.