.NET 中基于任务的异步模式(TAP):简介和概述

在 .NET 中,基于任务的异步模式是新开发的推荐异步设计模式。 它基于Task命名空间中的Task<TResult>类型和System.Threading.Tasks类型,用于表示异步作。

命名、参数和返回类型

TAP 使用单个方法来表示异步作的启动和完成。 这与异步编程模型(APM 或 IAsyncResult)模式和基于事件的异步模式(EAP)形成鲜明对比。 APM 需要 BeginEnd 方法。 EAP 需要具有 Async 后缀的方法,并且还需要一个或多个事件、事件处理程序委托类型和 EventArg派生类型。 TAP 中的异步方法在返回可等待类型(如 AsyncTaskTask<TResult>ValueTask)的方法的操作名称后面添加 ValueTask<TResult> 后缀。 例如,一个返回Get的异步Task<String>操作可以被命名为GetAsync。 如果要将 TAP 方法添加到已包含带有后缀的 Async EAP 方法名称的类,请改用后缀 TaskAsync 。 例如,如果类已有方法 GetAsync ,请使用名称 GetTaskAsync。 如果某个方法启动异步操作但不返回一种可等待的类型,则其名称应以BeginStart或其他动词开头,表明此方法不返回或抛出操作的结果。  

根据相应的同步方法是否返回 void 或类型 System.Threading.Tasks.Task,TAP 方法会返回 System.Threading.Tasks.Task<TResult>TResult

TAP 方法的参数应与其同步对应项的参数匹配,并且应按相同顺序提供。 但是, out 参数 ref 不受此规则的豁免,应完全避免。 应该将通过 outref 参数返回的所有数据改为作为由 TResult 返回的 Task<TResult> 的一部分返回,且应使用元组或自定义数据结构来容纳多个值。 此外,请考虑添加参数 CancellationToken ,即使 TAP 方法的同步对应项不提供参数。

专门用于创建、作或组合任务的方法(方法名称或方法所属类型的异步意图明确)不需要遵循此命名模式:此类方法通常称为 组合器。 组合器的示例包括 WhenAllWhenAny使用基于任务的异步模式一文的使用基于任务的内置组合器部分对此进行了介绍。

有关 TAP 语法与旧式异步编程模式(如异步编程模型(APM)和基于事件的异步模式(EAP)的语法有何不同的示例,请参阅 异步编程模式

启动异步操作

基于 TAP 的异步方法可以同步执行少量工作,例如验证参数和启动异步作,然后再返回生成的任务。 同步工作应保持在最低水平,以便异步方法可以快速返回。 快速返回的原因包括:

  • 可以从用户界面(UI)线程调用异步方法,任何长时间运行的同步工作都可能会损害应用程序的响应能力。

  • 可以同时启动多个异步方法。 因此,在异步方法的同步部分中的任何长时间运行的工作都可以延迟其他异步操作的启动,从而减少并发的优点。

在某些情况下,完成操作所需的工作量小于异步发动操作所需的工作量。 读取流时,按照在内存中已缓冲好的数据来满足该读取,这就是此类情形的一个示例。 在这种情况下,操作可能会同步完成,并可能返回一个已完成的任务。

例外

异步方法应引发仅将引发异步方法调用的异常,以响应用法错误。 生产代码中不应发生使用错误。 例如,如果将 null 引用(Nothing 在 Visual Basic 中)作为方法的参数之一传递会导致错误状态(通常由 ArgumentNullException 异常表示),则可以修改调用代码以确保永远不会传递 null 引用。 对于所有其他错误,在异步方法运行时发生的异常应分配给返回的任务,即使异步方法恰好在返回任务之前同步完成也是如此。 通常,任务最多包含一个异常。 但是,如果任务表示多个作(例如, WhenAll),则多个异常可能与单个任务相关联。

目标环境

实现 TAP 方法时,可以确定异步执行的位置。 可以选择在线程池上执行工作负荷,使用异步 I/O 实现该工作负荷(无需绑定到某个线程来执行大部分作),在特定的线程(如 UI 线程)上运行它,或使用任意数量的潜在上下文。 TAP 方法甚至可能没有任何操作要执行,并且可能只返回一个 Task,以表示系统中其他位置发生的某种情况(例如,任务表示数据到达了排队数据结构)。

TAP 方法的调用方可能会同步等待生成的任务,以阻止等待 TAP 方法完成,也可能会在异步操作完成时运行其他(延续)代码。 延续代码的创建者可以控制该代码的执行位置。 可以通过 Task 类的方法(例如,ContinueWith)显式创建延续代码,或者通过使用基于延续构建的语言支持隐式创建(例如,在 C# 中使用 await,在 Visual Basic 中使用 Await,在 F# 中使用 AwaitValue)延续代码。

任务状态

Task 类提供异步作的生命周期,该周期由 TaskStatus 枚举表示。 为了支持派生自 TaskTask<TResult> 的类型的角落案例,并支持构造与计划的分离,Task 类公开了 Start 方法。 由公共 Task 构造函数创建的任务称为冷任务,因为它们在未调度 Created 状态下开始其生命周期,并且仅当对这些实例调用Start时才进行调度。

所有其他任务都以热状态开始其生命周期,这意味着它们所代表的异步操作已被启动,其任务状态为除 TaskStatus.Created 之外的枚举值。 必须激活从 TAP 方法返回的所有任务。 如果 TAP 方法在内部使用任务的构造函数来实例化要返回的任务,则在返回该任务之前,TAP 方法必须在 Start 对象上调用 Task TAP 方法的使用者可以安全地假设返回的任务处于活动状态且不应尝试对从 TAP 方法返回的任何 Start 调用 Task。 对活动的任务调用 Start 将引发 InvalidOperationException 异常。

取消(可选)

在 TAP 中,对于异步方法实现者和异步方法使用者,取消是可选的。 如果操作允许取消,则会公开接受取消标记(CancellationToken 实例)的异步方法的重载。 按照约定,参数命名 cancellationToken

public Task ReadAsync(byte [] buffer, int offset, int count,
                      CancellationToken cancellationToken)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          cancellationToken As CancellationToken) _
                          As Task

该异步操作监视取消请求的此标记。 如果收到取消请求,可以选择接受该请求并取消操作。 如果取消请求导致工作过早结束,TAP 方法将返回一个任务,其结束状态为 Canceled;没有可用的结果,也不会抛出异常。 状态Canceled被视为任务的最终(已完成)状态,以及FaultedRanToCompletion状态。 因此,如果任务处于 Canceled 状态,则其 IsCompleted 属性返回 true。 在 Canceled 状态下完成任务时,将计划或执行向任务注册的任何延续,除非延续选项(如 NotOnCanceled)特定于取消延续。 任何通过使用语言功能异步等待已取消的任务的代码都将继续运行,但不接收 OperationCanceledException 或其中派生的异常。 通过诸如 Wait 的方法同步阻止的代码等待任务,并且 WaitAll 将继续运行但出现异常。

如果取消标记请求在接受调用标记的 TAP 方法之前取消,TAP 方法应返回 Canceled 任务。 但是,如果在异步作运行时请求取消,则异步作不需要接受取消请求。 仅当操作因取消请求而结束时,返回的任务才应以Canceled状态结束。 如果请求取消,但仍生成结果或异常,则任务应以RanToCompletionFaulted状态结束。

对于要首先对其公开可取消功能的异步方法,无需提供不接受取消令牌的重载。 对于无法取消的方法,请不要提供带有取消令牌的重载;这样可以帮助调用者更清楚地判断目标方法是否可以取消。 不需要取消的使用者代码可以调用一个方法,该方法会接受CancellationToken并使用None作为参数值。 None 在功能上等效于默认值 CancellationToken

进度报告(可选)

某些异步操作受益于提供进度通知;这些通知通常用于用异步操作的进度信息更新用户界面。

在 TAP 中,进度通过 IProgress<T> 接口进行处理,该接口作为通常命名 progress的参数传递给异步方法。 调用异步方法时提供进度接口有助于消除不正确使用导致的争用情况(也就是说,操作启动后未正确注册的事件处理程序可能缺少更新)。 更重要的是,根据所使用的代码,进度接口将支持不同的进度实现。 例如,使用的代码可能只关心最新的进度更新,或者需要缓冲所有更新,抑或希望为每个更新调用一个动作,或者想要控制调用是否分配到特定线程。 所有这些选项都可以通过使用接口的不同实现来实现,该实现可根据特定使用者的需求进行自定义。 与取消一样,TAP 实现应仅当 API 支持进度通知时提供 IProgress<T> 参数。

例如,如果 ReadAsync 本文前面讨论的方法能够以迄今为止读取的字节数的形式报告中间进度,则进度回调可能是一个 IProgress<T> 接口:

public Task ReadAsync(byte[] buffer, int offset, int count,
                      IProgress<long> progress)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          progress As IProgress(Of Long)) As Task

FindFilesAsync如果方法返回满足特定搜索模式的所有文件的列表,进度回调可以提供已完成工时百分比和当前部分结果集的估计值。 可通过元组来提供此信息:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
            string pattern,
            IProgress<Tuple<double,
            ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

也可以使用特定于 API 的数据类型执行此操作:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of FindFilesProgressInfo)) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

在后一种情况下,特殊数据类型通常后缀为 ProgressInfo

如果 TAP 实现提供接受 progress 参数的重载,则必须允许参数为 null,在这种情况下将不报告任何进度。 TAP 实现应同步向Progress<T>对象报告进度,这样可以使异步方法快速提供进度。 它还允许进度使用方确定处理信息的最佳方式和位置。 例如,进度实例可以选择将回调封送,并引发有关捕获到的同步上下文的事件。

IProgress<T> 实现

.NET 提供实现Progress<T>IProgress<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 事件的处理程序相同。 进度更新以异步方式进行,以避免在事件处理程序执行时延迟异步操作。 另一个 IProgress<T> 实现可以选择应用不同的语义。

选择要提供的重载

如果 TAP 实现同时使用可选 CancellationToken 参数和可选 IProgress<T> 参数,则可能需要最多四个重载:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

但是,许多 TAP 实现不提供取消或进度更新功能,因此需要采用一种单一的方法:

public Task MethodNameAsync(…);  
Public MethodNameAsync(…) As Task  

如果 TAP 实现支持取消或进度但不同时支持二者,则 TAP 实现可能提供以下两种重载:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
  
// … or …  
  
public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task  
  
' … or …  
  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task  

如果 TAP 实现同时支持取消和进度,则可以公开所有四个重载。 但是,它只能提供以下两个:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

为了补偿两个缺失的中间组合,开发人员可以为None参数传递CancellationTokencancellationToken默认值,为null参数传递progress

如果需要 TAP 方法的每种用法都支持取消或进度,则可以忽略不接受相关参数的重载。

如果决定公开多个重载以使取消或进度可选,则不支持取消或进度的重载的行为方式就像其已将取消的 None 或进度的 null 传递给确实支持它们的重载。

标题 DESCRIPTION
异步编程模式 介绍用于执行异步作的三种模式:基于任务的异步模式(TAP)、异步编程模型(APM)和基于事件的异步模式(EAP)。
实现基于任务的异步模式 介绍如何通过三种方式实现基于任务的异步模式(TAP):在 Visual Studio 中使用 C# 和 Visual Basic 编译器、手动或编译器和手动方法的组合。
使用基于任务的异步模式 描述你可以如何使用任务和回调实现等待,而无需阻止。
与其他异步模式和类型互作 介绍如何使用基于任务的异步模式(TAP)实现异步编程模型(APM)和基于事件的异步模式(EAP)。