基于任务的异步编程
基于任务的异步编程
基于任务的异步模式 (TAP) 是基于 System.Threading.Tasks
命名空间中的 System.Threading.Tasks.Task<TResult>
和 System.Threading.Tasks.Task
类型,这些类型用于表示任意异步操作。 TAP 是用于新开发的建议的异步设计模式。
命名、参数和返回类型
TAP 使用单个方法表示异步操作的开始和完成。 这与异步编程模型(APM 或 IAsyncResult)模式和基于事件的异步模式 (EAP) 形成对比。 APM 需要 Begin 和 End 方法。 EAP 需要后缀为 Async 的方法,以及一个或多个事件、事件处理程序委托类型和 EventArg 派生类型。
- 方法命名
TAP 中的异步方法名称应使用Async
后缀。若要将TAP方法添加到已包含带Async
后缀的EAP方法名称的类中,则应使用后缀TaskAsync
。例如,返回Task<string>
的Get
方法可命名为GetAsync
。如果类具有GetAsync
方法,则应该使用名称GetTaskAsync
。
- 方法参数
TAP 方法的参数应与其同步对应方法的参数匹配,并应以相同顺序提供。 不支持具有 out
、ref
参数,应避免使用。如果用到了 out
、ref
参数,则应该通过将数据封装为Task<TResult>
返回或使用元组来容纳多个值。即使 TAP 方法的同步对应方法没有提供 CancellationToken
参数,也应该考虑添加此参数。
- 返回类型
TAP 方法的返回类型有三种:Task
、Task<TResult>
、void
.若同步方法返回void
则应使用返回类型Task
,同步方法返回TResult
则应使用返回类型Task<TResult>
。应避免使用void
的返回类型,因为返回void
的异步方法具有特定用途(用于支持异步事件处理程序),具有不同的错误处理语义。
任务状态
Task
类提供了异步操作的生命周期,由 TaskStatus
枚举表示。
Created | 0 | 该任务已初始化,但尚未被计划。 |
WaitingForActivation | 1 | 该任务正在等待被激活并进行计划。 |
WaitingToRun | 2 | 该任务已被计划执行,但尚未开始执行。 |
Running | 3 | 该任务正在运行,但尚未完成。 |
WaitingForChildrenToComplete | 4 | 该任务已完成执行,正在隐式等待附加的子任务完成。 |
RanToCompletion | 5 | 已成功完成执行的任务。 |
Canceled | 6 | 该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 对取消进行了确认,此时该标记处于已发送信号状态;或者在该任务开始执行之前,已向该任务的 CancellationToken 发出了信号。 有关详细信息,请参阅任务取消。 |
Faulted | 7 | 由于未处理异常的原因而完成的任务。 |
为了支持派生自 Task
和 Task<TResult>
的类型的个别案例,并支持调度时分离构造,Task
类公开了 Start
方法。
公共 Task
构造函数创建的任务称为“冷任务” ,因为它们在非计划 Created
状态下开始生命周期,并仅在对这些实例调用 Start
时才被排入计划。
所有其他任务都属于“热任务”,意思就是异步操作已启动,并且其任务状态是 TaskStatus.Created
以外的枚举值。
如果方法内部使用构造函数实例化任务的化,那么必须在返回前调用Task
的Start
方法,否则任务不会启动并执行。TAP方法的使用者都默认任务已经处于活动状态,并且不会调用Start
。对活动的任务调用 Start
将引发 InvalidOperationException
异常。
异常
程序并不会总是执行的一帆风顺,当异步方法引发异常时,原始的调用环境跟当前的执行环境已经不在同一个上下文环境(线程)了。所以我们需要了解下TAP的异常处理机制。
TAP方法在调用时永远不会直接抛出异常。TAP 方法会返回Task
或者Task<TResult>
,方法内抛出的任何异常(包括从其他同步或者异步操作中传播过来的异常)都将传递到Task
或者Task<TResult>
对象中。
需要注意的是对于返回 void
的方法,由于没有 Task
对象,因此 void
方法引发的任何异常都会直接在 SynchronizationContext
上引发。
如果调用者使用Wait()
方法直接等待任务,则可得到一个包含所有异常的AggregateException
;但如果调用者使用await
,异常则会在等待时拆包异常,抛出第一个内部异常而不是AggregateException
。这个设计简化了开发难度,否则就必须在代码中捕捉AggregateException
检查异步内部异常,然后要么处理异常要么重新抛出,这会过于繁琐。
取消
在 TAP 中,取消是异步方法实现者和异步方法使用者的选项。 如果操作允许取消,则会公开接受取消标记(CancellationToken 实例)的异步方法的重载。 按照约定,该参数命名为 cancellationToken。
public Task ReadAsync(byte [] buffer, int offset, int count,
CancellationToken cancellationToken)
该异步操作将监视cancellationToken
,如果它收到取消请求,则可以选择接受该请求并取消操作。
如果在调用TAP方法之前取消,则TAP方法不会执行,将返回一个在Canceled
状态下结束的任务。
如果在运行异步操作时请求取消,该操作内部没有显式支持取消(操作内对cancellationToken
做了处理),则异步操作不会接受该取消请求。只有这个操作内部做了处理(如cancellationToken.ThrowIfCancellationRequested()
),返回的任务才以Canceled
状态结束。如果已请求取消,但仍然生成了结果或者异常,则返回一个在RanToCompletion
或Faulted
状态下结束的任务。
进度报告
在 TAP 中,通过 IProgress<T>
接口处理进度,此接口作为通常名为 progress
的参数传递给异步方法。 调用异步方法时提供进度接口有助于消除不正确使用导致的争用情况(也就是说,操作启动后未正确注册的事件处理程序可能缺少更新)。 更重要的是,根据所使用的代码,进度接口将支持不同的进度实现。
.NET Framework 4.5
提供单个 IProgress<T>
实现:Progress<T>
。 Progress<T>
类的声明方式如下:
public class Progress<T> : IProgress<T>
{
public Progress();
public Progress(Action<T> handler);
protected virtual void OnReport(T value);
public event EventHandler<T> ProgressChanged;
}
Progress<T>
的实例公开 ProgressChanged
事件,此事件在异步操作每次报告进度更新时引发。 实例化 ProgressChanged
实例后,会在捕获到的 SynchronizationContext
对象上引发 Progress<T>
事件。 如果没有可用的同步上下文,则使用针对线程池的默认上下文。 可以向此事件注册处理程序。 为了方便起见,也可将单个处理程序提供给 Progress<T>
构造函数,并且行为与 ProgressChanged
事件的事件处理程序一样。 异步引发进度更新以避免延迟异步操作,同时执行事件处理程序。
实现基于任务的异步模式
生成及使用TAP方法
- 使用编译器
自 .NET Framework 4.5
起,任何用 async
关键字修饰的方法都被视为异步方法,C#编译器
会执行必要的转换,来生成TAP方法。其实就是编译器帮我们自动生成了一个实现了IAsyncStateMachine
接口的状态机(StateMachine
),它在内部控制处理了Task
的状态。
- 手动生成TAP方法
你可以手动实现 TAP 模式以更好地控制实现。 编译器依赖从 System.Threading.Tasks
命名空间公开的公共外围应用和 System.Runtime.CompilerServices
命名空间中支持的类型。 如要自己实现 TAP,你需要创建一个 TaskCompletionSource<TResult>
对象、执行异步操作,并在操作完成时,调用 SetResult
、SetException
、SetCanceled
方法,或调用这些方法之一的Try版本。 手动实现 TAP 方法时,需在所表示的异步操作完成时完成生成的任务。 例如:
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
{
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try { tcs.SetResult(stream.EndRead(ar)); }
catch (Exception exc) { tcs.SetException(exc); }
}, state);
return tcs.Task;
}
使用await挂起执行
自 .NET Framework 4.5
起,可以使用 C#
中的 await
关键字来异步等待 Task
和 Task<TResult>
对象。等待 Task
时,await
表达式的类型为 void
。 等待 Task<TResult>
时,await
表达式的类型为 TResult
。 await
表达式必须出现在异步方法的正文内(即被 async
修饰的方法内)。
如果同步上下文(SynchronizationContext
对象)与暂停时正在执行异步方法的线程相关联,则异步方法使用上下文的 Post
方法,恢复相同的同步上下文。否则,它依赖暂停时的当前任务计划程序(TaskScheduler
对象)。通常情况下,TaskScheduler.Default
对象是线程池的计划程序(ThreadPoolTaskScheduler
)。此任务计划程序确定等待的异步操作是否应在操作完成时恢复,或是否应计划该恢复。默认计划程序通常允许在完成等待操作的线程上延续任务。
调用异步方法时,将同步执行函数的正文,直到遇见尚未完成的可等待实例上的第一个await
表达式,此时调用返回到调用方。如果异步方法不返回 void
,将会返回 Task
或 Task<TResult>
对象,以表示正在进行的计算。 在非 void
异步方法中,如果遇到 return
语句或到达方法正文末尾,任务就以 RanToCompletion
最终状态完成。 如果未经处理的异常导致无法控制异步方法正文,任务就以 Faulted
状态结束。 如果异常为 OperationCanceledException
,任务改为以 Canceled
状态结束。 通过此方式,最终将发布结果或异常。
还可以使用 Task.ConfigureAwait
方法,更好地控制异步方法中的暂停和恢复。 如前所述,默认情况下,异步方法挂起时会捕获当前上下文,捕获的上下文用于在恢复时调用异步方法的延续。 在多数情况下,这就是你所需的确切行为。 在其他情况下,你可能不关心延续上下文,则可以通过避免此类发布返回原始上下文来获得更好的性能。 若要启用此功能,请使用 Task.ConfigureAwait
方法,指示等待操作不要捕获和恢复上下文,而是继续执行正在等待完成的所有异步操作:
await someTask.ConfigureAwait(continueOnCapturedContext:false);