实现基于任务的异步模式Implementing the Task-based Asynchronous Pattern

可使用以下三种方式实现基于任务的异步模式 (TAP):使用 Visual Studio 中的 C# 和 Visual Basic 编译器、手动实现或编译器和手动方法相结合。You can implement the Task-based Asynchronous Pattern (TAP) in three ways: by using the C# and Visual Basic compilers in Visual Studio, manually, or through a combination of the compiler and manual methods. 以下各节详细地讨论了每一种方法。The following sections discuss each method in detail. 可以使用 TAP 模式实现计算密集型和 I/O 密集型异步操作。You can use the TAP pattern to implement both compute-bound and I/O-bound asynchronous operations. 工作负载部分介绍了各种类型的操作。The Workloads section discusses each type of operation.

生成 TAP 方法Generating TAP methods

使用编译器Using the compilers

自 .NET Framework 4.5 起,任何归于 async 关键字(Visual Basic 中的 Async)的方法都被视为异步方法,并且 C# 和 Visual Basic 编译器会执行必要的转换,以使用 TAP 异步实现方法。Starting with .NET Framework 4.5, any method that is attributed with the async keyword (Async in Visual Basic) is considered an asynchronous method, and the C# and Visual Basic compilers perform the necessary transformations to implement the method asynchronously by using TAP. 异步方法应返回 System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> 对象。An asynchronous method should return either a System.Threading.Tasks.Task or a System.Threading.Tasks.Task<TResult> object. 对于后者,函数的主体应返回 TResult,并且编译器确保此结果是通过生成的任务对象获得。For the latter, the body of the function should return a TResult, and the compiler ensures that this result is made available through the resulting task object. 同样,未在方法的主体中处理的任何异常都会被封送处理为输出任务并导致生成的任务结束以 TaskStatus.Faulted 状态结束。Similarly, any exceptions that go unhandled within the body of the method are marshaled to the output task and cause the resulting task to end in the TaskStatus.Faulted state. 此异常发生在 OperationCanceledException(或派生类型)未得到处理时,在这种情况下生成的任务以 TaskStatus.Canceled 状态结束。The exception is when an OperationCanceledException (or derived type) goes unhandled, in which case the resulting task ends in the TaskStatus.Canceled state.

手动生成 TAP 方法Generating TAP methods manually

你可以手动实现 TAP 模式以更好地控制实现。You may implement the TAP pattern manually for better control over implementation. 编译器依赖从 System.Threading.Tasks 命名空间公开的公共外围应用和 System.Runtime.CompilerServices 命名空间中支持的类型。The compiler relies on the public surface area exposed from the System.Threading.Tasks namespace and supporting types in the System.Runtime.CompilerServices namespace. 如要自己实现 TAP,你需要创建一个 TaskCompletionSource<TResult> 对象、执行异步操作,并在操作完成时,调用 SetResultSetExceptionSetCanceled 方法,或调用这些方法之一的Try版本。To implement the TAP yourself, you create a TaskCompletionSource<TResult> object, perform the asynchronous operation, and when it completes, call the SetResult, SetException, or SetCanceled method, or the Try version of one of these methods. 手动实现 TAP 方法时,需在所表示的异步操作完成时完成生成的任务。When you implement a TAP method manually, you must complete the resulting task when the represented asynchronous operation completes. 例如:For example:

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;
}
<Extension()>
Public Function ReadTask(stream As Stream, buffer() As Byte, 
                         offset As Integer, count As Integer, 
                         state As Object) As Task(Of Integer)
    Dim tcs As New TaskCompletionSource(Of Integer)()
    stream.BeginRead(buffer, offset, count, Sub(ar)
               Try  
                  tcs.SetResult(stream.EndRead(ar)) 
               Catch exc As Exception 
                  tcs.SetException(exc)
               End Try
            End Sub, state)
    Return tcs.Task
End Function   

混合方法Hybrid approach

你可能发现手动实现 TAP 模式、但将实现核心逻辑委托给编译器的这种方法很有用。You may find it useful to implement the TAP pattern manually but to delegate the core logic for the implementation to the compiler. 例如,当你想要验证编译器生成的异步方法之外的实参时,可能需要使用这种混合方法,以便异常可以转义到该方法的直接调用方而不是通过 System.Threading.Tasks.Task 对象被公开:For example, you may want to use the hybrid approach when you want to verify arguments outside a compiler-generated asynchronous method so that exceptions can escape to the method’s direct caller rather than being exposed through the System.Threading.Tasks.Task object:

public Task<int> MethodAsync(string input)
{
    if (input == null) throw new ArgumentNullException("input");
    return MethodAsyncInternal(input);
}

private async Task<int> MethodAsyncInternal(string input)
{

   // code that uses await goes here

   return value;
}
Public Function MethodAsync(input As String) As Task(Of Integer)
    If input Is Nothing Then Throw New ArgumentNullException("input")

    Return MethodAsyncInternal(input)
End Function

Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)

   ' code that uses await goes here
   
   return value
End Function

这种委托有用的另一种情况是:你在实施快速路径优化并想返回缓存的任务时。Another case where such delegation is useful is when you're implementing fast-path optimization and want to return a cached task.

工作负载Workloads

你可将计算密集型和 I/O 密集型异步操作作为 TAP 方法实现。You may implement both compute-bound and I/O-bound asynchronous operations as TAP methods. 但是,当 TAP 方法从库中公开地公开时,应仅向涉及 I/O 密集型操作的工作负载提供这种方法(它们也可能涉及计算,但不是应仅仅是计算)。However, when TAP methods are exposed publicly from a library, they should be provided only for workloads that involve I/O-bound operations (they may also involve computation, but should not be purely computational). 如果是纯粹的计算密集型方法,应只公开为同步实现。If a method is purely compute-bound, it should be exposed only as a synchronous implementation. 然后,使用它的代码可能会选择是将同步方法调用包装到任务中以将工作卸载到另一线程,还是实现并行。The code that consumes it may then choose whether to wrap an invocation of that synchronous method into a task to offload the work to another thread or to achieve parallelism. 如果方法是 I/O 密集型,应只公开为异步实现。And if a method is I/O-bound, it should be exposed only as an asynchronous implementation.

计算密集型任务Compute-bound tasks

System.Threading.Tasks.Task 类非常适合表示计算密集型操作。The System.Threading.Tasks.Task class is ideally suited for representing computationally intensive operations. 默认情况下,它利用 ThreadPool 类中的特殊支持来提供有效的执行,还对执行异步计算的时间、地点和方式提供重要控制。By default, it takes advantage of special support within the ThreadPool class to provide efficient execution, and it also provides significant control over when, where, and how asynchronous computations execute.

你可以通过以下方式生成计算密集型任务:You can generate compute-bound tasks in the following ways:

  • 在 .NET Framework 4 中,使用 TaskFactory.StartNew 方法,这种方法接受异步执行委托(通常是 Action<T>Func<TResult>)。In the .NET Framework 4, use the TaskFactory.StartNew method, which accepts a delegate (typically an Action<T> or a Func<TResult>) to be executed asynchronously. 如果你提供 Action<T> 委托,该方法会返回表示异步执行该委托的 System.Threading.Tasks.Task 对象。If you provide an Action<T> delegate, the method returns a System.Threading.Tasks.Task object that represents the asynchronous execution of that delegate. 如果你提供 Func<TResult> 委托,该方法会返回 System.Threading.Tasks.Task<TResult> 对象。If you provide a Func<TResult> delegate, the method returns a System.Threading.Tasks.Task<TResult> object. StartNew 方法的重载接受一个取消标记(CancellationToken)、任务创建选项(TaskCreationOptions)和一个任务计划程序(TaskScheduler),它们都对计划和任务执行提供细粒度控制。Overloads of the StartNew method accept a cancellation token (CancellationToken), task creation options (TaskCreationOptions), and a task scheduler (TaskScheduler), all of which provide fine-grained control over the scheduling and execution of the task. 定目标到当前任务计划程序的工厂实例可用作 Task 类的静态属性 (Factory);例如:Task.Factory.StartNew(…)A factory instance that targets the current task scheduler is available as a static property (Factory) of the Task class; for example: Task.Factory.StartNew(…).

  • 在 .NET Framework 4.5 及更高版本(包括 .NET Core 和 .NET Standard)中,使用静态 Task.Run 方法作为 TaskFactory.StartNew 的快捷方式。In the .NET Framework 4.5 and later versions (including .NET Core and .NET Standard), use the static Task.Run method as a shortcut to TaskFactory.StartNew. 你可以使用 Run 来轻松启动针对线程池的计算密集型任务。You may use Run to easily launch a compute-bound task that targets the thread pool. 在 .NET Framework 4.5 及更高版本中,这是用于启动计算密集型任务的首选机制。In the .NET Framework 4.5 and later versions, this is the preferred mechanism for launching a compute-bound task. 仅当需要更细化地控制任务时,才直接使用 StartNewUse StartNew directly only when you want more fine-grained control over the task.

  • 想要分别生成并计划任务时,请使用Task类型或Start方法的构造函数。Use the constructors of the Task type or the Start method if you want to generate and schedule the task separately. 公共方法必须仅返回已开始的任务。Public methods must only return tasks that have already been started.

  • 使用 Task.ContinueWith 方法的重载。Use the overloads of the Task.ContinueWith method. 此方法创建一项在另一任务完成时已排好计划的新任务。This method creates a new task that is scheduled when another task completes. 某些 ContinueWith 重载接受一个取消标记、延续选项和一个任务计划程序,以更好地控制计划和执行延续任务。Some of the ContinueWith overloads accept a cancellation token, continuation options, and a task scheduler for better control over the scheduling and execution of the continuation task.

  • 使用 TaskFactory.ContinueWhenAllTaskFactory.ContinueWhenAny 方法。Use the TaskFactory.ContinueWhenAll and TaskFactory.ContinueWhenAny methods. 这些方法会在提供的全部任务或任意一组任务完成时创建已计划的新任务。These methods create a new task that is scheduled when all or any of a supplied set of tasks completes. 这些方法还提供了重载,用于控制这些任务的计划和执行。These methods also provide overloads to control the scheduling and execution of these tasks.

在计算密集型任务中,如果系统在开始运行任务之前收到取消请求,则它可以防止执行已计划的任务。In compute-bound tasks, the system can prevent the execution of a scheduled task if it receives a cancellation request before it starts running the task. 同样,如果你提供一个取消标记(CancellationToken 对象),则可以将标记传递给监视该标记的异步代码。As such, if you provide a cancellation token (CancellationToken object), you can pass that token to the asynchronous code that monitors the token. 你也可以将此标记提供给先前提过的方法(如 StartNewRun),以便Task运行时也能监视该标记。You can also provide the token to one of the previously mentioned methods such as StartNew or Run so that the Task runtime may also monitor the token.

例如,请考虑使用呈现图像的异步方法。For example, consider an asynchronous method that renders an image. 任务的主体可以轮询取消标记,如果在呈现过程中收到取消请求,代码可提前退出。The body of the task can poll the cancellation token so that the code may exit early if a cancellation request arrives during rendering. 此外,如果呈现启动之前收到取消请求,你需要阻止呈现操作:In addition, if the cancellation request arrives before rendering starts, you'll want to prevent the rendering operation:

internal Task<Bitmap> RenderAsync(
              ImageData data, CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        var bmp = new Bitmap(data.Width, data.Height);
        for(int y=0; y<data.Height; y++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            for(int x=0; x<data.Width; x++)
            {
                // render pixel [x,y] into bmp
            }
        }
        return bmp;
    }, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As _
                            CancellationToken) As Task(Of Bitmap)
    Return Task.Run( Function()
                        Dim bmp As New Bitmap(data.Width, data.Height)
                        For y As Integer = 0 to data.Height - 1
                           cancellationToken.ThrowIfCancellationRequested()
                           For x As Integer = 0 To data.Width - 1
                             ' render pixel [x,y] into bmp
                           Next
                        Next
                        Return bmp
                     End Function, cancellationToken)
End Function

如果满足下列条件之一,则计算密集型任务以 Canceled 状态结束:Compute-bound tasks end in a Canceled state if at least one of the following conditions is true:

  • 取消请求通过 CancellationToken 对象到达,该对象在任务转换到 StartNew 状态前,作为创建方法的自变量(例如 RunRunning)提供。A cancellation request arrives through the CancellationToken object, which is provided as an argument to the creation method (for example, StartNew or Run) before the task transitions to the Running state.

  • OperationCanceledException 异常在此类任务的主体内未得到处理,该异常包含传给该任务的同一 CancellationToken,并且该标记显示已请求取消操作。An OperationCanceledException exception goes unhandled within the body of such a task, that exception contains the same CancellationToken that is passed to the task, and that token shows that cancellation is requested.

如果另一个异常在任务的主体内未得到处理,则此任务以 Faulted 状态结束,并且任何等待该任务或访问其结果的尝试都将引发异常。If another exception goes unhandled within the body of the task, the task ends in the Faulted state, and any attempts to wait on the task or access its result causes an exception to be thrown.

I/O 密集型任务I/O-bound tasks

若要创建一个不应由线程直接支持其全部执行的任务,请使用 TaskCompletionSource<TResult> 类型。To create a task that should not be directly backed by a thread for the entirety of its execution, use the TaskCompletionSource<TResult> type. 此类型公开一个返回关联 Task 实例的 Task<TResult> 属性。This type exposes a Task property that returns an associated Task<TResult> instance. 此任务的生命周期是由 TaskCompletionSource<TResult> 方法控制的,比如 SetResultSetExceptionSetCanceled 以及它们的 TrySet 变形。The life cycle of this task is controlled by TaskCompletionSource<TResult> methods such as SetResult, SetException, SetCanceled, and their TrySet variants.

假设你想创建一个将在指定时间段后完成的任务。Let's say that you want to create a task that will complete after a specified period of time. 例如,你可能想延迟用户界面中的活动。For example, you may want to delay an activity in the user interface. System.Threading.Timer 类已提供在指定时间段后以异步方式调用委托的能力,并且你可以通过使用 TaskCompletionSource<TResult>Task<TResult> 前端放在计时器上,例如:The System.Threading.Timer class already provides the ability to asynchronously invoke a delegate after a specified period of time, and by using TaskCompletionSource<TResult> you can put a Task<TResult> front on the timer, for example:

   public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
   {
       TaskCompletionSource<DateTimeOffset> tcs = null;
       Timer timer = null;

       timer = new Timer(delegate
       {
           timer.Dispose();
           tcs.TrySetResult(DateTimeOffset.UtcNow);
       }, null, Timeout.Infinite, Timeout.Infinite);

       tcs = new TaskCompletionSource<DateTimeOffset>(timer);
       timer.Change(millisecondsTimeout, Timeout.Infinite);
       return tcs.Task;
   }
   Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset) 
       Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
       Dim timer As Timer = Nothing
       
       timer = New Timer( Sub(obj)
                             timer.Dispose()
                             tcs.TrySetResult(DateTimeOffset.UtcNow)
                          End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

       tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
       timer.Change(millisecondsTimeout, Timeout.Infinite)
       Return tcs.Task
   End Function

从 .NET Framework 4.5 开始,Task.Delay 方法正是为此而提供的,并且你可以在另一个异步方法内使用它。例如,若要实现异步轮询循环:Starting with the .NET Framework 4.5, the Task.Delay method is provided for this purpose, and you can use it inside another asynchronous method, for example, to implement an asynchronous polling loop:

public static async Task Poll(Uri url, CancellationToken cancellationToken, 
                              IProgress<bool> progress)
{
    while(true)
    {
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
        bool success = false;
        try
        {
            await DownloadStringAsync(url);
            success = true;
        }
        catch { /* ignore errors */ }
        progress.Report(success);
    }
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken, 
                           progress As IProgress(Of Boolean)) As Task
    Do While True
        Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
        Dim success As Boolean = False
        Try
            await DownloadStringAsync(url)
            success = true
        Catch
           ' ignore errors
        End Try   
        progress.Report(success)
    Loop
End Function

TaskCompletionSource<TResult> 类并没有对应的非泛型类。The TaskCompletionSource<TResult> class doesn't have a non-generic counterpart. 然而 Task<TResult> 派生自 Task,因此你可以为仅返回任务的 I/O 密集型方法使用泛型 TaskCompletionSource<TResult> 对象。However, Task<TResult> derives from Task, so you can use the generic TaskCompletionSource<TResult> object for I/O-bound methods that simply return a task. 为了做到这一点,你可以使用具有虚拟 TResultBoolean 是一个很好的默认选项,但是如果你担心 Task 用户将其向下转换成 Task<TResult>,那么你可以转而使用私有 TResult 类型)。To do this, you can use a source with a dummy TResult (Boolean is a good default choice, but if you're concerned about the user of the Task downcasting it to a Task<TResult>, you can use a private TResult type instead). 例如,上一个示例中的 Delay 方法返回现有时间和所产生的偏移量(Task<DateTimeOffset>)。For example, the Delay method in the previous example returns the current time along with the resulting offset (Task<DateTimeOffset>). 如果结果值是不必要的,则可对该方法进行如下改写(注意对 TrySetResult 的返回类型的更改和实参的更改):If such a result value is unnecessary, the method could instead be coded as follows (note the change of return type and the change of argument to TrySetResult):

  public static Task<bool> Delay(int millisecondsTimeout)
  {
       TaskCompletionSource<bool> tcs = null;
       Timer timer = null;

       timer = new Timer(delegate
       {
           timer.Dispose();
           tcs.TrySetResult(true);
       }, null, Timeout.Infinite, Timeout.Infinite);

       tcs = new TaskCompletionSource<bool>(timer);
       timer.Change(millisecondsTimeout, Timeout.Infinite);
       return tcs.Task;
  }
  Public Function Delay(millisecondsTimeout As Integer) As Task(Of Boolean)
       Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
       Dim timer As Timer = Nothing

       Timer = new Timer( Sub(obj)
                             timer.Dispose()
                             tcs.TrySetResult(True)
                          End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

       tcs = New TaskCompletionSource(Of Boolean)(timer)
       timer.Change(millisecondsTimeout, Timeout.Infinite)
       Return tcs.Task
  End Function

计算密集型和 I/O 密集型混合任务Mixed compute-bound and I/O-bound tasks

异步方法不只局限于计算密集型或 I/O 密集型操作,还可以是两者的结合。Asynchronous methods are not limited to just compute-bound or I/O-bound operations but may represent a mixture of the two. 事实上,多个异步操作通常组合成较大的混合操作。In fact, multiple asynchronous operations are often combined into larger mixed operations. 例如,请考虑前面示例中的 RenderAsync 方法,该方法执行计算密集型操作以根据某些输入 imageData 呈现图像。For example, the RenderAsync method in a previous example performed a computationally intensive operation to render an image based on some input imageData. imageData 可能来自你异步访问的 Web 服务:This imageData could come from a web service that you asynchronously access:

public async Task<Bitmap> DownloadDataAndRenderImageAsync(
    CancellationToken cancellationToken)
{
    var imageData = await DownloadImageDataAsync(cancellationToken);
    return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(
             cancellationToken As CancellationToken) As Task(Of Bitmap) 
    Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
    Return Await RenderAsync(imageData, cancellationToken)
End Function

此示例还演示了如何通过多个异步操作使单个取消标记线程化。This example also demonstrates how a single cancellation token may be threaded through multiple asynchronous operations. 有关详细信息,请参阅使用基于任务的异步模式中的取消用法部分。For more information, see the cancellation usage section in Consuming the Task-based Asynchronous Pattern.

请参阅See also