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 处理状态机.
来源: https://www.cnblogs.com/smark/p/10159796.html