在现代软件开发中,异步编程已成为提升应用程序性能和响应性的关键技术。通过理解并实现可等待(awaitable)类型,可以扩展异步功能,使其适用于各种场景。本文将深入探讨await机制,并介绍几种实现可等待类型的不同方法,以及如何选择合适的实现方式。
在开发一个可等待的socket库的过程中,了解到了如何将可等待特性扩展到Task之外的其他类型。感谢社区中一些热心人士的帮助,偶然发现了Stephen Toub的文章,他的文章虽然简洁,但面向的是高级读者,阅读起来颇具挑战性。本文将重新审视他的代码,并努力使其更易于更广泛的开发者群体理解。
可等待类型至少包含一个名为GetAwaiter()的实例方法,该方法返回一个等待器(awaiter)类型的实例。这些也可以作为扩展方法实现。理论上,可以为int类型创建一个扩展方法,并使其返回一个表示当前异步操作的等待器,例如延迟指定的整数毫秒数。使用它就像await 1500一样,稍后将实现这一点。关键是,任何实现了GetAwaiter()(无论是直接实现还是通过扩展方法)并返回等待器对象的类型都可以被await。
GetAwaiter()返回的类型必须实现System.Runtime.CompilerServices.INotifyCompletion或相应的ICriticalNotifyCompletion接口。除了实现接口的OnCompleted()方法外,它还必须实现两个成员:IsCompleted和GetResult(),这两个成员不属于任何接口。
在静态类中,可以如下实现扩展方法:
internal static TaskAwaiter GetAwaiter(this int milliseconds) => Task.Delay(milliseconds).GetAwaiter();
现在可以对int类型进行await操作,它将等待指定的毫秒数。记住,任何启动Task的操作(如Task.Delay()所做的)都可以这样使用。如果操作不涉及Task,必须实现自己的等待器。让看看另一个类似于上述的示例:
public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan) {
return Task.Delay(timeSpan).GetAwaiter();
}
可以看到,这与上面的例子做了同样的事情,只是使用了TimeSpan而不是int,这意味着也可以对TimeSpan实例进行await操作。
有时,为了完成一个操作而创建一个Task是没有意义的。这可能是因为正在包装一个不使用Task的异步编程模式。也可能是因为操作本身很简单。如果使用所有struct类型作为可等待类型和/或等待器类型,它们将避免堆分配。据所知,运行Task至少需要在托管堆上分配一个对象。此外,Task之所以复杂,是因为它需要满足所有人的需求。真正想要的是一种简洁的方式来await。
在这种情况下,需要创建一个对象,实现以下两个接口之一:INotifyCompletion或INotifyCriticalCompletion。后者不会复制执行上下文,这意味着它可能更快,但非常危险,因为它可以提升代码的权限。通常,会想要使用前者,因为代码访问安全性的风险通常大于任何性能增益。
OnCompleted()方法在操作完成时被调用。这就是要做任何继续操作的地方。将在下面讨论。请注意,OnCompleted()应该是public的,以避免框架对struct进行装箱,它必须这样做才能访问接口。装箱会导致堆分配。如果方法是public的,它可以直接访问方法,而不需要装箱,相信是这样。还没有深入到IL中去验证,但这是有可能的,这样就可以高效地处理这种情况。
IsCompleted属性指示操作是否已完成。GetResult()方法不接受任何参数,其返回类型与伪任务的结果返回类型相同。如果没有结果,它可以是void。如果这相当于Task
试图想出一个创建自己的awaiter的好用例,不要太复杂,但遇到了困难。幸运的是,Sergey Tepliakov在这里提供了一个很好的例子,只需要稍微修改一下OnCompleted()和IsCompleted。将在下面探讨:
static class LazyUtility {
public struct Awaiter : INotifyCompletion {
private readonly Lazy _lazy;
public Awaiter(Lazy lazy) => _lazy = lazy;
public T GetResult() => _lazy.Value;
public bool IsCompleted => _lazy.IsValueCreated;
public void OnCompleted(Action continuation) {
if (null != continuation)
Task.Run(continuation);
}
}
public static Awaiter GetAwaiter(this Lazy lazy) {
return new Awaiter(lazy);
}
}
这将Lazy
请注意,在这里从未创建过Task。再次强调,GetResult()可以阻塞,就像在这里一样,如果Lazy
还要注意,在构造函数中接受了一个Lazy
可以看到,还在IsCompleted中转发了IsValueCreated。这让框架知道GetResult()中的工作是否已经完成,一旦Lazy
现在可以这样做:
var result = await myLazyT;
这将异步初始化myLazyT。应该指出的是,这个类中的任何操作都应该线程安全。Lazy