Поделиться через


Реализация асинхронного шаблона на основе задач

Асинхронный шаблон на основе задач можно реализовать тремя способами: с помощью компиляторов C# и Visual Basic в Visual Studio, вручную или с помощью сочетания методов компилятора и вручную. В следующих разделах подробно рассматриваются все методы. Шаблон TAP можно использовать для реализации асинхронных операций, связанных с вычислением, и операций ввода-вывода. В разделе "Рабочие нагрузки" рассматривается каждый тип операции.

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

Использование компиляторов

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

Другой случай, когда такое делегирование полезно, заключается в реализации оптимизации быстрого пути и необходимости возвращать кэшированную задачу.

Рабочие нагрузки

Вы можете реализовать асинхронные операции, привязанные к вычислениям, и операции ввода-вывода в виде методов TAP. Однако если методы TAP предоставляются публично из библиотеки, они должны быть предоставлены только для рабочих нагрузок, связанных с операциями ввода-вывода (они также могут включать вычисления, но не должны быть чисто вычислительными). Если метод является чисто вычислительным, он должен предоставляться только в виде синхронной реализации. Код, который использует его, может затем выбрать, следует ли упаковать вызов этого синхронного метода в задачу, чтобы выгрузить работу в другой поток или достичь параллелизма. И если метод привязан к вводу-выводу, его следует предоставлять только как асинхронную реализацию.

Задачи, связанные с вычислениями

Класс 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 объект), этот маркер можно передать в асинхронный код, отслеживающий маркер. Вы также можете предоставить маркер одному из ранее упомянутых методов, таких как StartNew или Run таким образом, что 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

Задачи, связанные с вычислениями, заканчиваются состоянием Canceled если выполнено хотя бы одно из следующих условий:

  • Запрос отмены поступает через CancellationToken объект, который предоставляется в качестве аргумента метода создания (например, StartNew или Run) перед переходом задачи в Running состояние.

  • Исключение OperationCanceledException остается необработанным внутри такой задачи, это исключение содержит то же CancellationToken, что передается задаче, и этот токен показывает, что запрошена отмена.

Если другое исключение остается необработанным в теле задачи, задача заканчивается в Faulted состоянии, и любые попытки ожидания завершения задачи или получения доступа к ее результату приводят к возникновению исключения.

Задачи, ограниченные вводом-выводом

Чтобы создать задачу, которая не должна быть непосредственно поддерживаемой потоком на протяжении всего её выполнения, используйте тип TaskCompletionSource<TResult>. Этот тип предоставляет Task свойство, возвращающее связанный Task<TResult> экземпляр. Жизненный цикл этой задачи управляется такими методами, как TaskCompletionSource<TResult>, SetResult, SetException, SetCanceled и их варианты TrySet.

Предположим, что вы хотите создать задачу, которая завершится через указанный период времени. Например, может потребоваться отложить действие в пользовательском интерфейсе. Класс 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> для методов ввода-вывода, которые просто возвращают задачу. Для этого можно использовать источник с фиктивным типом 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

Смешанные задачи с привязкой к вычислительным ресурсам и операции ввода-вывода

Асинхронные методы не ограничиваются только операциями, связанными с вычислениями или операциями ввода-вывода, но могут представлять собой смесь двух операций. На самом деле, несколько асинхронных операций часто объединяются в более крупные смешанные операции. Например, RenderAsync метод в предыдущем примере выполнил вычислительную операцию с интенсивным вычислением для отрисовки изображения на основе некоторых входных данных imageData. Это imageData может поступать из веб-службы, к которой вы обращаетесь асинхронно.

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

В этом примере также показано, как один маркер отмены может быть пронесен через несколько асинхронных операций. Для получения дополнительной информации см. раздел "Отмена задач" в Использование асинхронного шаблона на основе задач.

См. также