Реализация асинхронного шаблона, основанного на задачахImplementing the Task-based Asynchronous Pattern

Можно реализовать асинхронную модель на основе задач (TAP) тремя способами: с помощью компиляторов C# и Visual Basic в Visual Studio, вручную или путем сочетания этих методов.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 можно применять для создания асинхронных операций, связанных с операциями ввода-вывода и ограниченных по скорости вычислений.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.

Создание методов TAPGenerating TAP methods

С помощью компиляторовUsing the compilers

Начиная с версии .NET Framework 4.5 любой метод, который помечен ключевым словом async (Async в Visual Basic), считается асинхронным, и компиляторы 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.Task или System.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>, выполнить асинхронную операцию и после ее завершения вызвать метод SetResult, SetException или SetCanceled, или версии одного из этих методов 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

Может оказаться полезным реализовать шаблон TAР вручную, но делегировать основную логику для реализации компилятору.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

Реализацию асинхронных операций, связанных с вводом-выводом и ограниченных по скорости вычислений, можно выполнить как методы TAP.You may implement both compute-bound and I/O-bound asynchronous operations as TAP methods. Тем не менее, когда методы TAP предоставляются открыто из библиотеки, они должны быть предоставлены только для рабочих нагрузок, связанных с операциями ввода-вывода (они также могут включать в себя вычисления, но не должны быть исключительно вычислительными).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. Если метод имеет привязку к операциям ввода-вывода, его следует предоставлять только в асинхронной реализации.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. Экземпляр фабрики, предназначенный для текущего планировщика задач доступен как статическое свойство (Factory) из класса Task; Например: 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. Используйте StartNew непосредственно, только когда требуется более точный контроль над задачей.Use 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.ContinueWhenAll и TaskFactory.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. Можно также предоставить токен для одного из вышеупомянутых методов таких, как StartNew или Run для того, чтобы среда выполнения , 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 или Run) до того, как задача переходит в состояние Running.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-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>, такими как SetResult, SetException, SetCanceled, и их вариантами 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 и это дает возможность использовать универсальный объект 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. Чтобы это сделать, можно использовать источник с фиктивным TResult (Boolean — неплохой выбор по умолчанию, однако, если вас беспокоит пользователь 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

Смешанные задачи с привязкой к операциям ввода-вывода и ограниченные по скорости вычисленийMixed compute-bound and I/O-bound tasks

Асинхронные методы не ограничиваются только операциями, связанными с вводом-выводом или ограниченными по скорости вычислений, а могут представлять собой их комбинацию.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 могло бы быть получено из веб-службы, к которой осуществляется асинхронный доступ: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