Partilhar via


Implementando o padrão assíncrono baseado em tarefas

Você pode implementar o padrão assíncrono baseado em tarefas (TAP) de três maneiras: usando os compiladores C# e Visual Basic no Visual Studio, manualmente ou por meio de uma combinação dos métodos compilador e manual. As seções a seguir discutem cada método em detalhes. Você pode usar o padrão TAP para implementar operações assíncronas ligadas à computação e E/S. A seção Cargas de trabalho discute cada tipo de operação.

Geração de 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 o último, o corpo da função deve retornar um TResult, e o compilador garante que esse resultado seja disponibilizado através do objeto de tarefa resultante. Da mesma forma, quaisquer exceções que não sejam tratadas no corpo do método são agrupadas para a tarefa de saída e fazem com que a tarefa resultante termine no TaskStatus.Faulted estado. A exceção a essa regra é quando um OperationCanceledException (ou tipo derivado) não é manipulado, caso em que a tarefa resultante termina no TaskStatus.Canceled estado.

Geração manual de métodos TAP

Você pode implementar o padrão TAP manualmente para um melhor controle sobre a implementação. O compilador depende da área de superfície pública exposta do System.Threading.Tasks namespace e dos tipos de suporte no System.Runtime.CompilerServices namespace. Para implementar o TAP você mesmo, crie um TaskCompletionSource<TResult> objeto, execute a operação assíncrona e, quando ela for concluída, chame o SetResultmétodo , SetExceptionou SetCanceled ou a Try versão de um desses 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 para a implementação ao compilador. Por exemplo, você pode querer 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 através 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 em cache.

Cargas de Trabalho

Você pode implementar operações assíncronas ligadas à computação e à E/S como métodos TAP. No entanto, quando os métodos TAP são expostos publicamente a partir de uma biblioteca, devem ser fornecidos apenas para cargas de trabalho que envolvam operações ligadas a E/S (também podem envolver computação, mas não devem ser puramente computacionais). Se um método é puramente ligado à computação, ele deve 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 obter paralelismo. E se um método estiver vinculado a E/S, ele deverá ser exposto apenas como uma implementação assíncrona.

Tarefas ligadas à computação

A System.Threading.Tasks.Task classe é ideal para representar operações computacionais intensivas. Por padrão, ele aproveita o suporte especial dentro da ThreadPool classe para fornecer 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 vinculadas à 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 direcionada ao pool de threads. Este é o mecanismo preferido para iniciar uma tarefa vinculada à computação. Use StartNew diretamente apenas 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 método aceitam um token de cancelamento (CancellationToken), opções de criação de tarefas (TaskCreationOptions) e um agendador de StartNew tarefas (TaskScheduler), que fornecem controle refinado sobre o agendamento e a execução da tarefa. Uma instância de fábrica destinada ao agendador de tarefas atual está disponível como uma propriedade estática (Factory) da Task classe, por exemplo: Task.Factory.StartNew(…).

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

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

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

Em tarefas ligadas à computação, o sistema pode impedir a execução de uma tarefa agendada se receber uma solicitação de cancelamento antes de começar a executar a tarefa. Como tal, 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 para um dos métodos mencionados anteriormente, como StartNew ou Run para que o Task tempo de execução 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 mais cedo 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, convém evitar 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 da transição da tarefa para o Running estado.

  • Uma OperationCanceledException exceção não é tratada dentro do corpo de tal tarefa, essa exceção contém o mesmo CancellationToken que é passado para a tarefa e esse token mostra que o cancelamento é solicitado.

Se outra exceção não for tratada no corpo da tarefa, a tarefa termina no Faulted estado, e qualquer tentativa de aguardar a tarefa ou acessar seu resultado fará com que uma exceção seja lançada.

Tarefas ligadas a E/S

Para criar uma tarefa que não deve ser apoiada diretamente por um thread durante toda a sua 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 desta tarefa é controlado por TaskCompletionSource<TResult> métodos como SetResult, SetException, SetCancelede suas TrySet variantes.

Digamos que você queira criar uma tarefa que será concluída após um período de tempo especificado. Por exemplo, você pode querer atrasar uma atividade na interface do usuário. A System.Threading.Timer classe já fornece a capacidade de invocar um delegado de forma assíncrona após um período de tempo especificado e, ao usá-lo TaskCompletionSource<TResult> , você pode colocar uma Task<TResult> frente no 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íncrona:

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 uma contrapartida não genérica. No entanto, Task<TResult> deriva de Task, para que você possa usar o objeto genérico TaskCompletionSource<TResult> para métodos vinculados a E/S que simplesmente retornam uma tarefa. Para fazer isso, você pode usar uma fonte com um manequim TResult (Boolean é uma boa escolha padrão, mas se você estiver preocupado com o Task usuário do downcasting para um Task<TResult>, você pode usar um tipo privado TResult em vez disso). Por exemplo, o Delay método no exemplo anterior retorna a hora atual junto com o deslocamento resultante (Task<DateTimeOffset>). Se tal valor de resultado for desnecessário, o método pode, em vez disso, ser codificado da seguinte forma (observe a mudança do tipo de retorno e a mudança 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 mistas ligadas à computação e à E/S

Os métodos assíncronos não se limitam apenas a operações ligadas a computação ou E/S, mas podem representar uma mistura dos dois. Na verdade, várias operações assíncronas são frequentemente 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 através de várias operações assíncronas. Para obter mais informações, consulte a seção de uso de cancelamento em Consumindo o padrão assíncrono baseado em tarefas.

Consulte também