基于Task的异步模式的定义
命名,参数和返回类型
在TAP(Task-based Asynchronous Pattern)中的异步操作的启动和完成是通过一个单独的方法来表现的,因此只有一个方法要命名。这与IAsyncResult模式或者APM(Asynchronous Programming Model,异步编程模型)模式形成对比,后者必须要有开始方法名和结束方法名;还与基于事件(event-based)的异步模式(EAP)不同,它们要求方法名以Async为后缀,而且要求一个或多个事件,事件句柄委托类型和派生自Event参数的类型。TAP中的异步方法使用“Async”后缀命名,跟在操作名称的后面(例如MethodNameAsync)。TAP中的异步方法返回一个Task类型或者Task
比如,思考下面的“Read”方法,它将特定数量的数据读取到一个以特定偏移量的buffer中:
public class MyClass
{
public int Read(byte [] buffer, int offset, int count);
}
这个方法对应的APM版本则有下面两个方法:
public class MyClass
{
public IAsyncResult BeginRead(byte[] buffer, int offset, int count,AsyncCallback callback, object state);
public int EndRead(IAsyncResult asyncResult);
}
EAP版本对应的方法是这样的:
public class MyClass
{
public void ReadAsync(byte[] buffer, int offset, int count);
public event ReadCompletedEventHandler ReadCompleted;
}
public delegate void ReadCompletedEventHandler(object sender, ReadCompletedEventArgs eventArgs);
public class ReadCompletedEventArgs: AsyncCompletedEventArgs
{
public int Result {
get;
}
}
TAP对应的版本只有下面一个方法:
public class MyClass
{
public Task
}
一个基本的TAP方法的参数应该和同步方法的参数相同,且顺序相同。然而,“out”和“ref”参数不遵从这个规则,并且应该避免使用它们。通过out或者ref返回的任何数据可以作为返回的Task
纯粹致力于创建,操作,或组合的任务方法(该方法的异步目的在方法名上或者在方法上以类型命名是明确的)不需要遵循上述命名模式;这些方法通常被称为"组合子"。这种方法的例子包括Task. WhenAll和Task.WhenAny,本文档后面的会更深入地讨论。
表现
初始化异步操作
在返回结果的任务之前,基于TAP异步方法允许同步地处理少量的工作。这项工作应保持在所需的最低数量,执行如验证参数和启动异步操作的操作。很可能从用户界面线程将调用异步方法,因此所有长时间运行的异步方法的同步前期部分工作可能会损害响应能力。很有可能同时将启动多个异步方法,因此所有长时间运行的异步方法的同步前期部分工作可能会推迟启动其他异步操作,从而减少并发的好处。
在某些情况下,完成操作所需的工作量小于异步启动操作需要的工作量(例如,从流中读取数据,这个读取操作可以被已经缓冲在内存中的数据所满足)。在这种情况下,操作可能同步完成,返回一个已经完成的任务。
异常
一个异步方法只应该直接捕获一个MethodNameAsync 调用时抛出的异常以响应用法错误。对于其他所有的错误,在异步方法执行期间发生的异常应该分配给返回的任务。这种情况是在Task返回之前,异步方法同步完成下发生的。一般地,一个Task至多包含一个异常。然而,对于一个Task表示多个操作(如,Task.WhenAll)的情况,单个Task也会关联多个异常。
【*每个.Net设计指南都指出,一个用法错误可以通过改变调用方法的码来避免。比如,当把null作为一个方法的参数传递时,错误状态就会发生,错误条件通常被表示为ArgumentNullException,开发者可以修改调用码来确保null没有传递过。换言之,开发者可以并且应该确保用法错误从来没有在生产代码中发生过】。
目标环境
异步执行的发生取决于TAP方法的实现。TAP方法的开发人员可能选择在线程池上执行工作负载,也可能选择使用异步 I/O实现它,因而没有被绑定到大量操作执行的线程上,也可以选择在特定的线程上运行,如UI线程,或者其他一些潜在的上下文。甚至可能是这种情况,TAP方法没有东西执行,简单返回一个在系统中其他地方情况发生的Task(如Task
TAP方法的调用者也可能阻塞等待TAP方法的完成(通过在结果的Task上同步地等待),或者利用延续在异步操作完成时执行附加代码。延续创建者在延续代码执行的地方有控制权。这些延续代码要么通过Task类(如ContinueWith)显示地创建,要么使用语言支持隐式地建立在延续代码之上(如C#中的“await”)。
Task状态
Task类提供了异步操作的生命周期,该生命周期通过TaskStatus枚举表示。为了支持派生自Task和Task
所有从TAP方法返回的tasks肯定是“热的”。如果TAP方法内部使用一个Task的构造函数来实例化要返回的task,那么此TAP方法必须在返回task之前在Task对象上调用Start方法。TAP方法的消费者可以安全地假定返回的task是“热的”,并不应该尝试在任何返回自TAP方法的Task上调用Start。在“热的”task上调用Start会导致InvalidOperationException (Task类自动处理这个检查)。
可选:撤销
TAP中的撤销对于异步方法的实现者和异步方法的消费者都是选择加入的。如果一个操作将要取消,那么它会暴露
一个接受System.Threading.CancellationToken的MethodNameAsync 的重载。异步操作会监视对于撤销请求的这个token,如果接收到了撤销请求,可以选择处理该请求并取消操作。如果处理请求导致任务过早地结束,那么从TAP方法返回的Task会以TaskStatus.Canceled状态结束。
为了暴露一个可取消的异步操作,TAP实现提供了在同步对应的方法的参数后接受一个CancellationToken的重载。按照惯例,该参数命名为“cancellationToken”。
public Task
byte [] buffer, int offset, int count,
CancellationToken cancellationToken);
如果token已经请求了撤销并且异步操作尊重该请求,那么返回的task将会以TaskStatus.Canceled状态结束,将会产生没有可利用的Result,并且没有异常。Canceled状态被认为是一个伴随着Faulted和RanToCompletion 状态的任务最终或完成的状态。因此,Canceled 状态的task的IsCompleted 属性返回true。当一个Canceled 状态的task完成时,任何用该task注册的延续操作都会被调度或执行,除非这些延续操作通过具体的TaskContinuationOptions 用法在被创建时取消了(如TaskContinuationOptions.NotOnCanceled)。任何异步地等待一个通过语言特性使用的撤销的task的代码将会继续执行并且收到一个OperationCanceledException(或派生于该异常的类型)。在该task(通过Wait 或WaitAll方法)上同步等待而阻塞的任何代码也会继续执行并抛出异常。
如果CancellationToken已经在接受那个token的TAP方法调用之前发出了取消请求,那么该TAP方法必须返回一个Canceled状态的task。然而,如果撤销在异步操作执行期间请求,那么异步操作不需要尊重该撤销请求。只有由于撤销请求的操作完成时,返回的Task才会以Canceled 状态结束。如果一个撤销被请求了,但是结果或异常仍产生了,那么Task将会分别以RanToCompletion或 Faulted 的状态结束。
首先,在使用异步方法的开发者心目中,那些渴望撤销的方法,需要提供一个接受CancellationToken变量的重载。对于不可取消的方法,不应该提供接受CancellationToken的重载。这个有助于告诉调用者目标方法实际上是否是可取消的。不渴望撤销的消费者可以调用一个接受CancellationToken的方法来把CancellationToken.None作为提供的参数值。CancellationToken.None功能上等价于default(CancellationToken)。
可选:进度报告
一些异步操作得益于提供的进度通知,一般利用这些进度通知来更新关于异步操作进度的UI。
在TAP中,进度通过IProgress
比如,如果我们上面提到的ReadAsync方法可以以迄今读取字节数的形式能报告中间的进度,那么进度的回调(callback)可以是一个IProgress
public Task
byte [] buffer, int offset, int count,
IProgress
如果FindFilesAsync方法返回一个所有文件的列表,该列表满足一个特殊的搜索模式,那么进度回调可以提供完成工作的百分比和当前部分结果集的估计。它也可以这样处理元组,如:
public Task
string pattern,
IProgress
或者使用API具体的数据类型,如:
public Task
string pattern,
IProgress
在后一种情况,特殊的数据类型以“ProgressInfo”为后缀。
如果TAP实现提供了接受progress参数的重载,那么它们必须允许参数为null,为null的情况下,进度不会报告。TAP实现应该同步地报告IProgress
IProgreee
Progress
public class Progress
{
public Progress();
public Progress(Action
protected virtual void OnReport(T value);
public event EventHandler
}
Progress
如何选择提供的重载函数
有了CancellationToken和IProgress
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
public Task MethodNameAsync(…, IProgress
public Task MethodNameAsync(…,
CancellationToken cancellationToken, IProgress
然而,因为它们没有提供cancellation和progress的能力,许多TAP实现有了最短的重载的需求:
public Task MethodNameAsync(…);
如果一个实现支持cancellation或者progress但不同时支持,那么TAP实现可以提供2个重载:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
// … or …
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, IProgress
如果实现同时支持cancellation和progress,那么它可以默认提供4个重载。然而,只有2个有效:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…,
CancellationToken cancellationToken, IProgress
为了得到那2个遗失的重载,开发者可以通过给CancellationToken参数传递CancellationToken.None(或者default(CancellationToken))和/或给progress参数传递null。
如果期望TAP方法的每一种用法都应该使用cancellation和/或progress,那么不接受相关参数的重载可以忽略。
如果TAP方法的多个重载公开了可选的cancellation和/或progress,那么不支持cancellation和/或progress的重载的表现应该像支持他们的重载已经传递了CancellationToken.None和null分别给cancellation和progress一样。
实现基于Task的异步模式
生成方法
编译器生成
在.NET Framework 4.5中,C#编译器实现了TAP。任何标有async关键字的方法都是异步方法,编译器会使用TAP执行必要的转换从而异步地实现方法。这样的方法应该返回Task或者Task
手动生成
开发者可以手动地实现TAP,就像编译器那样或者更好地控制方法的实现。编译器依赖来自System.Threading.Tasks命名空间暴露的公开表面区域(和建立在System.Threading.Tasks之上的System.Runtime.CompilerServices中支持的类型),还有对开发者直接可用的功能。当手动实现TAP方法时,开发者必须保证当异步操作完成时,完成返回的Task。
混合生成
在编译器生成的实现中混合核心逻辑的实现,对于手动实现TAP通常是很有用的。比如这种情况,为了避免方法直接调用者产生而不是通过Task暴露的异常,如:
public Task
{
if (input == null) throw new ArgumentNullException("input");
return MethodAsyncInternal(input);
}
private async Task
{
… // code that uses await
}
参数应该在编译器生成的异步方法之外改变,这种委托有用的另一种场合是,当一个“快速通道”优化可以通过返回一个缓存的task来实现的时候。
工作负荷
计算受限和I/O受限的异步操作可以通过TAP方法实现。然而,当TAP的实现从一个库公开暴露时,应该只提供给包含I/O操作的工作负荷(它们也可以包含计算,但不应该只包含计算)。如果一个方法纯粹受计算限制,它应该只通过一个异步实现暴露,消费者然后就可以为了把该任务卸载给其他的线程的目的来选择是否把那个同步方法的调用包装成一个Task,并且/或者来实现并行。
计算限制
Task类最适合表示计算密集型操作。默认地,为了提供有效的执行操作,它利用了.Net线程池中特殊的支持,同时也对异步计算何时,何地,如何执行提供了大量的控制。
生成计算受限的tasks有几种方法。
在.Net 4中,启动一个新的计算受限的task的主要方法是TaskFactory.StartNew(),该方法接受一个异步执行的委托(一般来说是一个Action或者一个Func
在.Net 4.5中,Task类型暴露了一个静态的Run方法作为一个StartNew方法的捷径,可以很轻松地使用它来启动一个作用在线程池上的计算受限的task。从.Net 4.5开始,对于启动一个计算受限的task,这是一个更受人喜欢的机制。当行为要求更多的细粒度控制时,才直接使用StartNew。
Task类型公开了构造函数和Start方法。如果必须要有分离自调度的构造函数,这些就是可以使用的(正如先前提到的,公开的APIs必须只返回已经启动的tasks)。
Task类型公开了多个ContinueWith的重载。当另外一个task完成的时候,该方法会创建新的将被调度的task。该重载接受CancellationToken,TaskCreationOptions,和TaskScheduler,这些都对task的调度和执行提供了细粒度的控制。
TaskFactory类提供了ContinueWhenAll 和ContinueWhenAny方法。当提供的一系列的tasks中的所有或任何一个完成时,这些方法会创建一个即将被调度的新的task。有了ContinueWith,就有了对于调度的控制和任务的执行的支持。
思考下面的渲染图片的异步方法。task体可以获得cancellation token为的是,当渲染发生的时候,如果一个撤销请求到达后,代码可能过早退出。而且,如果一个撤销请求在渲染开始之前发生,我们也可以阻止任何的渲染。
public Task
ImageData data, CancellationToken cancellationToken)
{
return Task.Run(() =>
{
var bmp = new Bitmap(data.Width, data.Height);
for(int y=0; y
cancellationToken.ThrowIfCancellationRequested();
for(int x=0; x
… // render pixel [x,y] into bmp
}
}
return bmp;
}, cancellationToken);
}
如果下面的条件至少一个是正确的,计算受限的tasks会以一个Canceled状态的结束:
在Task过度到TaskStatus.Running状态之前,CancellationToken为一个发出撤销请求的创建方法的参数提供(如StartNew,Run)。
有这样的一个Task,它内部有未处理的OperationCanceledException。该OperationCanceledException 包含和CancellationToken属性同名的CancellationToken传递到该Task,且该CancellationToken已经发出了撤销请求。
如果该Task体中有另外一个未经处理的异常,那么该Task就会以Faulted的状态结束,同时在该task上等待的任何尝试或者访问它的结果都将导致抛出异常。
I/O限制
使用TaskCompletionSource
思考这样的需求,创建一个在特定的时间之后会完成的task。比如,当开发者在UI场景中想要延迟一个活动一段时间时,这可能使有用的。.NET中的System.Threading.Timer类已经提供了这种能力,在一段特定时间后异步地调用一个委托,并且我们可以使用TaskCompletionSource
public static Task
{
var tcs = new TaskCompletionSource
new Timer(self =>
{
((IDisposable)self).Dispose();
tcs.TrySetResult(DateTimeOffset.UtcNow);
}).Change(millisecondsTimeout, -1);
return tcs.Task;
}
在.Net 4.5中,Task.Delay()就是为了这个目的而生的。比如,这样的一个方法可以使用到另一个异步方法的内部,以实现一个异步的轮训循环:
public static async Task Poll(
Uri url,
CancellationToken cancellationToken,
IProgress
{
while(true)
{
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
bool success = false;
try
{
await DownloadStringAsync(url);
success = true;
}
catch { /* ignore errors */ }
progress.Report(success);
}
}
没有TaskCompletionSource
public static Task Delay(int millisecondsTimeout)
{
var tcs = new TaskCompletionSource
new Timer(self =>
{
((IDisposable)self).Dispose();
tcs.TrySetResult(true);
}).Change(millisecondsTimeout, -1);
return tcs.Task;
}
混合计算限制和I/O限制的任务
异步方法不是仅仅受限于计算受限或者I/O受限的操作,而是可以代表这两者的混合。实际上,通常情况是不同性质的多个异步操作被组合在一起生成更大的混合操作。比如,思考之前的RenderAsync方法,该方法基于一些输入的ImageData执行一个计算密集的操作来渲染一张图片。该ImageData可能来自于一个我们异步访问的Web服务:
public async Task
CancellationToken cancellationToken)
{
var imageData = await DownloadImageDataAsync(cancellationToken);
return await RenderAsync(imageData, cancellationToken);
}
这个例子也展示了一个单独的CancellationToken是如何通过多个异步操作被线程化的。