深入async/await知多少
? ? ? .net的async/await功能相信對很多人來說并不陌生了,有人感覺這功能很好,但也有人說這功能不好容易產生一些莫名其妙的死鎖;有人說這些異步功能也有人說這是同步功能。其實在使用async/await的有多少人真的了解它們呢?接下來詳細地講述它們和在什么情況下需要注意的細節。
為什么需要它
? ? ??如果你對async/await的需求不明顯,那只能說明你平時很少寫異步邏輯(當你很少接觸傳統異常邏輯編寫的情況下,最好也找些相關功能用一下先也了解一下傳統異常調整用情況)。在傳統Begin/End的異步模式中所有結果都是由回調函數來處理,當一個邏輯有多層異步代碼時那整個業務邏輯代碼混合著各大和業務無關的回調函數中游走,這會讓代碼變更難以維護;當有了async/await后這種方式得到了解脫,因為你再不需 在不同Callback函數了維護一簡單的邏輯了;取而代之的就是我們平時普通方法調用一樣,只是在調用間加個await關鍵字。
工作原理
? ? ? async/await簡單來說只是一個語法糧,它只是告訴編譯器要把這些代碼編譯成一個異步狀態機。它的基礎工作接口如下:
public interface INotifyCompletion{void OnCompleted(Action continuation);}當然只有這個接口還不足以讓編譯去構建一個Awaiter,還要擴展兩方法
? ? ? IAwaiterObject必須繼承INotifyCompletion接口,這樣就可以構成了一個最簡單Awaiter規則,只要方法返回IAwaiterObject即可以使用await關鍵字進行異步處理。那編譯器是怎樣生成其對應的異步代碼的呢?可以通過以下代碼你就能更好地理解。
var?item?=?await GetData(); Console.Write(item)以上的await代碼你可以解決為
action=>Console.Write(item); if(awaiter(getdata(),action).completed())action();編譯會包裝一個狀態機,當getdata是同步完成時接下來就調用action方法,如果getdata是異步完成由其內異步回調方法來調用action方法.
Task是什么?
? ? ? async/await如果每一步都要自己的封裝那這個功能使用門檻就非常高了,為也讓這功能更好地使用所以.net提供了一個async/await的基礎實現,那就是Task.Task提供一系列完善的功能主要包括:自有的awaiter線程調度器,wait同步等待行主和TaskCompletionSource<T>等一系列簡化async/await使用自定義擴展需求功能。
同步還是異步?
? ? ? async/await是一個異步處理模型,但并不能說明所有的async/await都是異步處理;具體要看Awaiter狀態機是由誰觸發的,當上層方法邏輯是同步或IO同步完成的情況那await后面的代碼則由同當前線程觸發執行,如果上層方法是異步完成的情況下則由對應相關異步完成的線程調用;所以async/await也有些情況是同步完成的,只是這種情況在IO處理上并不多見.
await后面代碼由什么線程執行?
? ? ??有部分人認為只要使用async/await那就肯定是多線程處理,其實這并不完全正確,前面提到了即使IO也有同步完成的時候,所以是存在相關代碼都由當前線程來處理的.async/await最基礎就是狀態機異步回調,所以await后面的代碼肯定是由回調線程來執行,如果起始的awaiter是Task.Run觸發那后面的代碼則于Task內部線程池來處理;在調用IO的時候其實相樣后期代碼則有IO回調線程來完成。
? ? ? 其實在實際運行過程中,方法中一連串的await代碼是由一個或多個線程來共同完成;為什么會存在這情況呢,主要還是和相關awaiter下層實現有關系。如果由同一個線程執行那說明相關方法都同步完成了。那多個線程完成的是什么情況呢?主要還是方法中有多個IO await代碼塊,而每個IO都是異步完成的這樣就會導致后面每個await代碼塊都是由上一個IO回調線程來處理。
? ? ? 實際使用是否自己控制?一般基礎的實現都是由實現者來管理回調線程,不過自己可以在中間加入一個TaskCompletionSource<T>代理返回就可以控制后續工作線程的運作了,通過TaskCompletionSource<T>后就可以制定自己的線程隊列機制來觸發Completed了。
改造傳統異步方法
? ? ??如果有的邏輯還是基于Begin/End方法,但基礎方法又不提供async/await怎辦呢?雖然可以自己封裝一個Awaiter來處理,但簡單有效的辦法就是使用TaskCompletionSource<T>對象;可以在一個方法并返回對應的Task
public Task<int> Add() {TaskCompletionSource<int> result = new TaskCompletionSource<int>();//beginreturn result.Task; }后面的工作就是在End方法調用相關的方法即可
說實話TaskCompletionSource<T>這個對象設計成泛型還真不好用,畢竟在一引起底層設計中用時候很難固定這個T的,特別是在反射調用await的情況下,在這里分享一個擴展支持object設置的TaskCompletionSource<T>類;
class AnyCompletionSource<T> : TaskCompletionSource<T>, IAnyCompletionSource, IInvokeTimeOut{public AnyCompletionSource(){ID = System.Threading.Interlocked.Increment(ref mID);}public long TimeOutElapsed { get; set; } = 10000;static long mID;public long ID { get; private set; }private int mCompleted = 0;public Action<IAnyCompletionSource> Completed { get; set; }public Action<IAnyCompletionSource> TimeOut { get; set; }public void Success(object data){if (System.Threading.Interlocked.CompareExchange(ref mCompleted, 1, 0) == 0){TrySetResult((T)data);OnCompleted();}}public void InvokeTimeOut(){if (TimeOut != null){TimeOut(this);}else{if (System.Threading.Interlocked.CompareExchange(ref mCompleted, 1, 0) == 0){TrySetException(new TimeoutException($"{this.GetType()} process time out!"));}}}private void OnCompleted(){try{Completed?.Invoke(this);}catch{ }finally{}}public void Error(Exception error){if (System.Threading.Interlocked.CompareExchange(ref mCompleted, 1, 0) == 0){TrySetException(error);OnCompleted();}}public Task GetTask(){return this.Task;}public async void Wait<Result>(Task<Result> task, Action<IAnyCompletionSource, Task<Result>> action){try{await task;action(this, task);}catch (Exception e_){Error(e_);}}public async void Wait<Result>(Task<Result> task){try{await task;Success(task.Result);}catch (Exception e_){Error(e_);}}public async void Wait(Task task, Action<IAnyCompletionSource> action){try{await task;action(this);}catch (Exception e_){Error(e_);}}}如何在反射方法中使用await?
? ? 相信比較少的朋友會這樣用,實際上做基礎方法代理的時候是需碰到的在這里也簡單地說一下。由于Method.Invokd返回的是object,所以無法針對返回值為object的方法進行await的;其實await并不是針對方法行為的,而是針對方法的返回值,所以簡單地轉換一下對象再await即可
var result = handler.MethodInfo.Invoke(obj, arg); if?(result?is?Task?task)await?task;為何有假死現象?
? ? ??有些人說使用async/await程序容易出現莫名其妙的假死現像,其實這種情況的出現主要是使用了Task.wait有關系;主要原因是使用了沒有指定超時的wait行為,假設Task.wait是等待下一個awaiter狀態通知,但這個時候又調用了Task.wait等待,結果導致當前線程回歸到一下狀態執行環節結果導致死現像。
? ? ??以下針對socket的接收為例:receive->awaiter->wait 以上行為的wait會導致線無法回歸到下一次begin receive,既然數據無法繼續接收那接下來的狀態等待自然就無法得到通知了,這就會造了常見的假死現像!
使用原則
? ? ??所以在使用async/await的時候最好不要混合同步等待方法,普通開發者最好遵循要么就全用要么就不用原則(混用風險非常大,如果要用記住加個wait timeout)。如你是一個熟悉的開發者,其awaiter回調線程又是自己控制的那就可以適當的采用,即使這樣還是容易進坑的!
public virtual void OnCompleted(ResultType type, string message){if (System.Threading.Interlocked.CompareExchange(ref mCompletedStatus, 1, 0) == 0){Result.Status = ResultStatus.Completed;Client.TcpClient.DataReceive = null;Client.TcpClient.ClientError = null;Result.ResultType = type;Result.Messge = message;Host?.Push(Client);Completed?.Invoke(this);ResultDispatch.DispatchCenter.Enqueue(this,?3);}}以上就是一坑代碼,由于由于指令用了wait(),結果導致調度中心隊列狀態回歸問題阻塞了。。。后來針對存在wait()需求的指令采用其他方法觸發狀態機才能解決問題
public virtual void OnCompleted(ResultType type, string message){if (System.Threading.Interlocked.CompareExchange(ref mCompletedStatus, 1, 0) == 0){Result.Status = ResultStatus.Completed;Client.TcpClient.DataReceive = null;Client.TcpClient.ClientError = null;Result.ResultType = type;Result.Messge = message;Host?.Push(Client);Completed?.Invoke(this);if (Command.GetType() == typeof(SELECT) || Command.GetType() == typeof(AUTH)){Task.Run(() => TaskCompletion());}else{ResultDispatch.DispatchCenter.Enqueue(this, 3);}}}這些問題說真的非常不好排查,所以沒有特別的情況還是遵循這相關原則好!
async void xxx()
? ? ??說實話真不建議用這樣的方法(不過有時這種方式用起來挺方便的),這種方法會引起狀態機斷層;斷層意味著這方法的異常無法往上拋,就是說調用這些方法的外層方法無法try住這方法異常!如果這些方法內沒有進行try那不好意思那就會導致程序直接結束。所以在使用中最好用async Task來代替它,這樣可以讓狀態機再往上層拋,這時候只需要在最頂層try即可。如果是要用那就確認方法內try來處理任何可引發異常的代碼。
總結
? ? ??以上講述了async/await的基礎原理、使用和一些問題;對于async/await的觀點我是強烈建議使用,現有的API方法都已經完全支持了特別是IO接口無一不支持,這樣好的異步驅動代碼模型沒有理由不用的!即使不是異步IO代碼也可以通過ValueTask來解決Task帶來對象開銷過大的問題,所以對于.net的開發者來說應該適應它!
總結
以上是生活随笔為你收集整理的深入async/await知多少的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用C#在STM32上写第一个Hello
- 下一篇: 利用Azure Functions和k8