实现基于任务的异步模式

可以通过三种方式实现基于任务的异步模式(TAP):在 Visual Studio 中使用 C# 和 Visual Basic 编译器、手动或编译器和手动方法的组合。 以下各节详细介绍了每个方法。 可以使用 TAP 模式实现计算绑定和 I/O 绑定异步操作。 “工作负荷”部分讨论每种作类型。

生成 TAP 方法

使用编译器

从 .NET Framework 4.5 开始,任何使用关键字 async 标记的方法(在 Visual Basic 中为 Async)都被视为异步方法,C# 和 Visual Basic 编译器进行必要的转换,以通过 TAP 异步实现该方法。 异步方法应返回一个 System.Threading.Tasks.Task 或一个 System.Threading.Tasks.Task<TResult> 对象。 对于后者,函数的主体应返回一个 TResult,编译器可确保通过生成的任务对象提供此结果。 同样,任何在方法主体内未被处理的异常都会被封送到输出任务,并导致生成的任务以 TaskStatus.Faulted 状态结束。 此规则的例外情况是 OperationCanceledException(或派生类型)未经处理,在这种情况下,生成的任务以TaskStatus.Canceled状态结束。

手动创建 TAP 方法

可以手动实现 TAP 模式,以便更好地控制实现。 编译器依赖从 System.Threading.Tasks 命名空间公开的公共外围应用和 System.Runtime.CompilerServices 命名空间中支持的类型。 若要自行实现 TAP,请创建一个TaskCompletionSource<TResult>对象,执行异步操作,并在它完成时调用SetResultSetExceptionSetCanceled方法,或者调用这些方法之一的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;
}
<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

混合方法

你可能会发现,手动实现 TAP 模式很有用,但将实现的核心逻辑委托给编译器。 例如,当你想要验证编译器生成的异步方法之外的参数时,可能需要使用这种混合方法,以便异常可以转义到该方法的直接调用方而不是通过 System.Threading.Tasks.Task 对象被公开:

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

另一种情况是,当你实现快速路径优化并想要返回缓存的任务时,这种委派非常有用。

工作量

可以将计算密集型和 I/O 密集型异步操作实现为 TAP 方法。 但是,当 TAP 方法从库中公开地公开时,应仅向涉及 I/O 密集型操作的工作负载提供这种方法(它们也可能涉及计算,但不是应仅仅是计算)。 如果是纯粹的计算密集型方法,应只公开为同步实现。 然后,使用该代码的代码可以选择是将该同步方法的调用包装到任务中,将工作卸载到另一个线程,还是实现并行度。 如果方法是 I/O 密集型,应只公开为异步实现。

计算密集型任务

System.Threading.Tasks.Task 类非常适合用于表示计算密集型作。 默认情况下,它利用类中的 ThreadPool 特殊支持来提供高效的执行,它还提供对异步计算的执行时间、位置和方式的重要控制。

可以通过以下方式生成计算绑定任务:

  • 在 .NET Framework 4.5 及更高版本中(包括 .NET Core 和 .NET 5+),使用静态 Task.Run 方法作为快捷方式 TaskFactory.StartNew。 您可以使用 Run,轻松启动面向线程池的计算密集型任务。 这是启动计算绑定任务的首选机制。 仅当需要对任务进行更精细的控制时,才直接使用 StartNew

  • 在 .NET Framework 4 中,使用 TaskFactory.StartNew 方法,该方法接受一个委托(通常是 Action<T>Func<TResult>)以异步方式执行。 如果提供 Action<T> 委托,该方法将返回一个 System.Threading.Tasks.Task 对象,该对象表示该委托的异步执行。 如果提供 Func<TResult> 委托,该方法将返回一个 System.Threading.Tasks.Task<TResult> 对象。 StartNew 方法的重载接受取消令牌(CancellationToken)、任务创建选项(TaskCreationOptions)和任务调度程序(TaskScheduler),所有这些都提供了对任务调度和执行的精细化控制。 目标当前任务调度器的工厂实例可作为Factory类的静态属性(Task)获得,例如:Task.Factory.StartNew(…)

  • 请使用Task类型和Start方法的构造函数,单独生成和计划任务。 公共方法必须仅返回已开始的任务。

  • 使用 Task.ContinueWith 方法的重载。 此方法创建一个新任务,该任务会在另一个任务完成时被安排。 某些 ContinueWith 重载接受一个取消标记、延续选项和一个任务计划程序,以更好地控制计划和执行延续任务。

  • 使用TaskFactory.ContinueWhenAll方法和TaskFactory.ContinueWhenAny方法。 这些方法会在提供的全部任务或任意一组任务完成时创建已计划的新任务。 这些方法还提供了重载,用于控制这些任务的计划和执行。

在计算绑定任务中,如果系统在开始运行任务之前收到取消请求,则系统可以阻止执行计划任务。 因此,如果提供取消令牌(CancellationToken 对象),可以将该令牌传递给监视令牌的异步代码。 还可以将令牌提供给上述方法之一,例如 StartNewRun ,这样 Task 运行时也可以监视令牌。

例如,请考虑呈现图像的异步方法。 任务的主体可以轮询取消标记,如果在呈现过程中收到取消请求,代码可提前退出。 此外,如果取消请求在渲染开始前到达,则需要阻止渲染操作。

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

如果以下条件中至少有一个为 true,计算绑定任务会以 Canceled 状态结束:

  • 取消请求通过CancellationToken对象到达,在任务转换为StartNew状态之前,该对象作为创建方法(例如或 RunRunning)的参数提供。

  • OperationCanceledException 异常在此类任务的主体内未得到处理,该异常包含传给该任务的同一 CancellationToken,并且该标记显示已请求取消操作。

如果在任务执行过程中未处理另一个异常,任务将结束于Faulted状态,任何等待任务或访问其结果的尝试都会导致抛出异常。

I/O 密集型任务

如果您需要创建一个在整个执行过程中不应由线程直接支持的任务,请使用TaskCompletionSource<TResult>类型。 此类型公开一个返回关联 Task 实例的 Task<TResult> 属性。 此任务的生命周期由TaskCompletionSource<TResult>方法(如SetResultSetException及其SetCanceledTrySet变体)控制。

假设你要创建一个任务,该任务将在指定时间段后完成。 例如,你可能想要延迟用户界面中的活动。 System.Threading.Timer 类已提供在指定时间段后以异步方式调用委托的能力,并且你可以通过使用 TaskCompletionSource<TResult>Task<TResult> 前端放在计时器上,例如:

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

此方法 Task.Delay 用于此目的,可以在另一个异步方法中使用此方法,例如,实现异步轮询循环:

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> 类没有非泛型对应项。 但是, Task<TResult> 派生自 Task,因此,可以将泛型 TaskCompletionSource<TResult> 对象用于仅返回任务的 I/O 绑定方法。 为此,可以使用带有虚拟 TResult 的源(Boolean 是一个非常好的默认选择,但如果担心用户将 Task 向下转换为 Task<TResult>,则可以改用专用 TResult 类型)。 例如, Delay 上一示例中的方法返回当前时间以及生成的偏移量(Task<DateTimeOffset>)。 如果不需要这样的结果值,可以改为按如下方式对方法进行编码(请注意返回类型的更改和参数 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 密集型任务

异步方法不仅限于计算绑定或 I/O 绑定操作,而是可能表示这两种操作的混合体。 事实上,多个异步操作通常合并成更大的混合操作。 例如,RenderAsync上一个示例中的方法执行了一个计算密集型操作,根据某些输入imageData渲染图像。 imageData 可能来自您异步访问的 Web 服务。

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

此示例还演示了如何通过多个异步操作穿线单个取消令牌。 有关详细信息,请参阅 “使用基于任务的异步模式”中的取消使用部分。

另请参阅