.net的async/await功能相信对很多人来说并不陌生了,有人感觉这功能很好,但也有人说这功能不好容易产生一些莫名其妙的死锁;有人说这些异步功能也有人说这是同步功能。其实在使用async/await的有多少人真的了解它们呢?接下来详细地讲述它们和在什么情况下需要注意的细节。
为什么需要它
如果你对async/await的需求不明显,那只能说明你平时很少写异步逻辑(当你很少接触传统异常逻辑编写的情况下,最好也找些相关功能用一下先也了解一下传统异常调整用情况)。在传统Begin/End的异步模式中所有结果都是由回调函数来处理,当一个逻辑有多层异步代码时那整个业务逻辑代码混合着各大和业务无关的回调函数中游走,这会让代码变更难以维护;当有了async/await后这种方式得到了解脱,因为你再不需 在不同Callback函数了维护一简单的逻辑了;取而代之的就是我们平时普通方法调用一样,只是在调用间加个await关键字。
工作原理
async/await简单来说只是一个语法粮,它只是告诉编译器要把这些代码编译成一个异步状态机。它的基础工作接口如下:
public interface INotifyCompletion{void OnCompleted(Action continuation);}
当然只有这个接口还不足以让编译去构建一个Awaiter,还要扩展两方法
public interface IAwaiterObject : INotifyCompletion{IAwaiterObject GetAwaiter();object GetResult();}
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方法调用相关的方法即可
public void SetCanceled();public void SetException(IEnumerable<Exception> exceptions);public void SetException(Exception exception);public void SetResult(TResult result);public bool TrySetCanceled();public bool TrySetCanceled(CancellationToken cancellationToken);public bool TrySetException(IEnumerable<Exception> exceptions);public bool TrySetException(Exception exception);public bool TrySetResult(TResult result);
说实话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的开发者来说应该适应它!