Compartilhar via


Implementando o padrão assíncrono baseado em tarefa

Você pode implementar o TAP (Padrão Assíncrono baseado em tarefa) de três maneiras: usando os compiladores C# e Visual Basic no Visual Studio manualmente ou por meio de uma combinação do compilador e dos métodos manuais. As seções a seguir discutem cada método em detalhes. Você pode usar o padrão TAP para implementar operações assíncronas limitadas pela computação e por operações de entrada/saída. A seção Cargas de Trabalho discute cada tipo de operação.

Gerando métodos TAP

Usando os compiladores

A partir do .NET Framework 4.5, qualquer método atribuído com a async palavra-chave (Async no Visual Basic) é considerado um método assíncrono, e os compiladores C# e Visual Basic executam as transformações necessárias para implementar o método de forma assíncrona usando TAP. Um método assíncrono deve retornar um System.Threading.Tasks.Task ou um System.Threading.Tasks.Task<TResult> objeto. Para este último, o corpo da função deve retornar um TResult, e o compilador garante que esse resultado seja disponibilizado por meio do objeto de tarefa resultante. Da mesma forma, quaisquer exceções que não sejam tratadas no corpo do método são encaminhadas para a tarefa de saída e fazem com que a tarefa resultante termine no estado TaskStatus.Faulted. A exceção a essa regra é quando um OperationCanceledException (ou tipo derivado) fica sem tratamento, nesse caso, a tarefa resultante termina no TaskStatus.Canceled estado.

Gerando métodos TAP manualmente

Você pode implementar o padrão TAP manualmente para melhor controle sobre a implementação. O compilador depende da área pública exposta pelo namespace System.Threading.Tasks e pelos tipos de suporte no namespace System.Runtime.CompilerServices. Para implementar o TAP por conta própria, crie um objeto TaskCompletionSource<TResult>, execute a operação assíncrona e, quando for concluída, chame o método SetResult, SetException ou SetCanceled, ou a versão Try de um destes métodos. Ao implementar um método TAP manualmente, você deve concluir a tarefa resultante quando a operação assíncrona representada for concluída. Por exemplo:

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

Abordagem híbrida

Você pode achar útil implementar o padrão TAP manualmente, mas delegar a lógica principal da implementação ao compilador. Por exemplo, talvez você queira usar a abordagem híbrida quando quiser verificar argumentos fora de um método assíncrono gerado pelo compilador para que as exceções possam escapar para o chamador direto do método em vez de serem expostas por meio do System.Threading.Tasks.Task objeto:

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

Outro caso em que essa delegação é útil é quando você está implementando a otimização de caminho rápido e deseja retornar uma tarefa armazenada em cache.

Cargas de trabalho

Você pode implementar operações assíncronas limitadas pela computação e limitadas pela E/S como métodos TAP. No entanto, quando os métodos TAP são expostos publicamente de uma biblioteca, eles devem ser fornecidos apenas para cargas de trabalho que envolvem operações associadas a E/S (eles também podem envolver computação, mas não devem ser puramente computacionais). Se um método for puramente associado à computação, ele deverá ser exposto apenas como uma implementação síncrona. O código que o consome pode então escolher se deseja encapsular uma invocação desse método síncrono em uma tarefa para descarregar o trabalho para outro thread ou para alcançar o paralelismo. E se um método estiver associado a E/S, ele deverá ser exposto apenas como uma implementação assíncrona.

Tarefas associadas à computação

A System.Threading.Tasks.Task classe é ideal para representar operações computacionalmente intensivas. Por padrão, ele aproveita o suporte especial dentro da ThreadPool classe para fornecer uma execução eficiente e também fornece controle significativo sobre quando, onde e como os cálculos assíncronos são executados.

Você pode gerar tarefas associadas à computação das seguintes maneiras:

  • No .NET Framework 4.5 e versões posteriores (incluindo .NET Core e .NET 5+), use o método estático Task.Run como um atalho para TaskFactory.StartNew. Você pode usar Run para iniciar facilmente uma tarefa vinculada à computação que utiliza o pool de threads. Esse é o mecanismo preferencial para iniciar uma tarefa associada à computação. Use StartNew diretamente somente quando quiser um controle mais refinado sobre a tarefa.

  • No .NET Framework 4, use o TaskFactory.StartNew método, que aceita um delegado (normalmente um Action<T> ou um Func<TResult>) para ser executado de forma assíncrona. Se você fornecer um Action<T> delegado, o método retornará um System.Threading.Tasks.Task objeto que representa a execução assíncrona desse delegado. Se você fornecer um Func<TResult> delegado, o método retornará um System.Threading.Tasks.Task<TResult> objeto. As sobrecargas do StartNew método aceitam um token de cancelamento (CancellationToken), opções de criação de tarefa (TaskCreationOptions) e um agendador de tarefas (TaskScheduler), todos os quais fornecem controle refinado sobre o agendamento e a execução da tarefa. Uma instância de fábrica direcionada ao agendador de tarefas atual está disponível como uma propriedade estática (Factory) da Task classe; por exemplo: Task.Factory.StartNew(…).

  • Use os construtores do Task tipo e o Start método se você quiser gerar e agendar a tarefa separadamente. Os métodos públicos só devem retornar tarefas que já foram iniciadas.

  • Use as sobrecargas do método Task.ContinueWith. Esse método cria uma nova tarefa agendada quando outra tarefa é concluída. Algumas das sobrecargas ContinueWith aceitam um token de cancelamento, opções de continuação e um agendador de tarefas para obter melhor controle sobre o agendamento e a execução da tarefa de continuação.

  • Use os métodos TaskFactory.ContinueWhenAll e TaskFactory.ContinueWhenAny. Esses métodos criam uma nova tarefa agendada quando todo ou qualquer um dos conjuntos de tarefas fornecido é concluído. Esses métodos também fornecem sobrecargas para controlar o agendamento e a execução dessas tarefas.

Em tarefas associadas à computação, o sistema poderá impedir a execução de uma tarefa agendada se receber uma solicitação de cancelamento antes de começar a executar a tarefa. Dessa forma, se você fornecer um token de cancelamento (CancellationToken objeto), poderá passar esse token para o código assíncrono que monitora o token. Você também pode fornecer o token a um dos métodos mencionados anteriormente, como StartNew ou Run para que o Task runtime também possa monitorar o token.

Por exemplo, considere um método assíncrono que renderiza uma imagem. O corpo da tarefa pode sondar o token de cancelamento para que o código possa sair antecipadamente se uma solicitação de cancelamento chegar durante a renderização. Além disso, se a solicitação de cancelamento chegar antes do início da renderização, você desejará impedir a operação de renderização:

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

As tarefas associadas à computação terminam em um Canceled estado se pelo menos uma das seguintes condições for verdadeira:

  • Uma solicitação de cancelamento chega por meio do CancellationToken objeto, que é fornecido como um argumento para o método de criação (por exemplo, StartNew ou Run) antes que a tarefa faça a transição para o Running estado.

  • Uma exceção OperationCanceledException ficará sem tratamento dentro do corpo dessa tarefa, se a exceção contiver o mesmo CancellationToken que é passado para a tarefa e se o token mostrar que o cancelamento foi solicitado.

Se outra exceção não for tratada no corpo da tarefa, a tarefa terminará no estado Faulted e qualquer tentativa de aguardar a tarefa ou acessar seu resultado provocará uma exceção.

Tarefas associadas à E/S

Para criar uma tarefa que não deve ser apoiada diretamente por um thread durante toda a execução, use o TaskCompletionSource<TResult> tipo. Esse tipo expõe uma Task propriedade que retorna uma instância associada Task<TResult> . O ciclo de vida dessa tarefa é controlado por TaskCompletionSource<TResult> métodos como SetResult, SetExceptione SetCanceledsuas TrySet variantes.

Digamos que você queira criar uma tarefa que será concluída após um período de tempo especificado. Por exemplo, talvez você queira atrasar uma atividade na interface do usuário. System.Threading.Timer A classe já fornece a capacidade de invocar assíncronamente um delegado após um período de tempo especificado e, usando TaskCompletionSource<TResult>, você pode colocar um Task<TResult> na frente do temporizador, por exemplo:

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

O Task.Delay método é fornecido para essa finalidade e você pode usá-lo dentro de outro método assíncrono, por exemplo, para implementar um loop de sondagem assíncrono:

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

A TaskCompletionSource<TResult> classe não tem um equivalente não genérico. No entanto, Task<TResult> deriva de Task, para que você possa usar o objeto genérico TaskCompletionSource<TResult> para métodos associados a E/S que simplesmente retornam uma tarefa. Para fazer isso, é possível usar uma fonte com um TResult fictício (Boolean é uma boa opção padrão, mas se você estiver preocupado sobre o usuário que fará downcast da Task para uma Task<TResult>, poderá, em vez disso, usar um tipo TResult particular). Por exemplo, o Delay método no exemplo anterior retorna o tempo atual junto com o deslocamento resultante (Task<DateTimeOffset>). Se esse valor de resultado for desnecessário, o método poderá ser codificado da seguinte maneira (observe a alteração do tipo de retorno e a alteração do argumento para 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

Tarefas de computação mistas envolvendo processamento e E/S

Os métodos assíncronos não se limitam apenas a operações associadas à computação ou de E/S, mas podem representar uma mistura dos dois. Na verdade, várias operações assíncronas geralmente são combinadas em operações mistas maiores. Por exemplo, o RenderAsync método em um exemplo anterior executou uma operação computacionalmente intensiva para renderizar uma imagem com base em alguma entrada imageData. Isso imageData pode vir de um serviço Web que você acessa de forma assíncrona:

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

Este exemplo também demonstra como um único token de cancelamento pode ser encadeado por meio de várias operações assíncronas. Para saber mais, veja a seção de uso de cancelamento em Consumindo o padrão assíncrono baseado em tarefa.

Consulte também