Implementação do padrão assíncrono baseado em tarefas

Pode implementar o padrão assíncrono baseado em tarefas (TAP) de três formas: usando os compiladores C# e Visual Basic no Visual Studio, manualmente, ou através 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 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 à palavra-chave async (Async em Visual Basic) é considerado um método assíncrono. Os compiladores C# e Visual Basic realizam as transformações necessárias para implementar o método de forma assíncrona, usando o 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.

Início da tarefa e eliminação da tarefa

Use Start apenas para tarefas criadas explicitamente com um construtor Task que ainda se encontram no estado Created. Os métodos TAP públicos devem devolver tarefas ativas, pelo que os chamadores não devem precisar de chamar Start.

Na maioria dos códigos TAP, não descartes tarefas. A Task não detém recursos não geridos no caso típico, e descartar cada tarefa acrescenta custos sem benefício prático. Descarte apenas quando APIs ou medições específicas indiquem necessidade.

Se iniciares um processo em segundo plano que perdure além do percurso imediato da chamada, mantém a responsabilidade explícita e segue a conclusão. Para mais orientações, veja Manter os métodos assíncronos vivos.

Geração manual de métodos TAP

Podes implementar manualmente o padrão TAP para melhor controlo da 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:

static class StreamExtensions
{
    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;
    }
}
Module StreamExtensions
    <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
End Module

Abordagem híbrida

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

class Calculator
{
    private int value = 0;

    public Task<int> MethodAsync(string input)
    {
        if (input == null) throw new ArgumentNullException(nameof(input));
        return MethodAsyncInternal(input);
    }

    private async Task<int> MethodAsyncInternal(string input)
    {
        // code that uses await goes here
        await Task.Delay(1);
        return value;
    }
}
Class Calculator
    Private value As Integer = 0

    Public Function MethodAsync(input As String) As Task(Of Integer)
        If input Is Nothing Then Throw New ArgumentNullException(NameOf(input))
        Return MethodAsyncInternal(input)
    End Function

    Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)
        ' code that uses await goes here
        Await Task.Delay(1)
        Return value
    End Function
End Class

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

Pode implementar tanto operações assíncronas limitadas por computação como operações assíncronas limitadas por E/S como métodos TAP. No entanto, quando expõe métodos TAP publicamente a partir de uma biblioteca, forneça-os apenas para cargas de trabalho que envolvam operações limitadas por I/O. Estas operações também podem envolver computação, mas não devem ser puramente computacionais. Se um método for puramente ligado ao cálculo, exponha-o apenas como uma implementação síncrona. O código que o consome pode então escolher se envolve uma invocação desse método síncrono numa tarefa para transferir o trabalho para outro thread ou para alcançar paralelismo. Se um método for ligado à I/O, expõe-o apenas como uma implementação assíncrona.

Tarefas ligadas à computação

A System.Threading.Tasks.Task classe funciona bem para representar operações computacionalmente intensivas. Por padrão, aproveita o suporte especial da classe ThreadPool para garantir uma execução eficiente. Também proporciona controlo significativo sobre quando, onde e como as computações assíncronas são executadas.

Gerar tarefas limitadas por computação das seguintes formas:

  • 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. Use Run para lançar facilmente uma tarefa ligada ao cálculo que vise o pool de threads. Este método é o mecanismo preferido para lançar uma tarefa limitada ao cálculo. Use StartNew diretamente apenas quando quiser um controle mais refinado sobre a tarefa.

  • No .NET Framework 4, use o método TaskFactory.StartNew. Aceita um delegado (tipicamente um Action<T> ou um Func<TResult>) para executar 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 tarefas (TaskCreationOptions) e um agendador de tarefas (TaskScheduler). Estes parâmetros proporcionam um controlo detalhado sobre o agendamento e execução da tarefa. Uma instância de fábrica que visa o agendador de tarefas atual está disponível como uma propriedade estática (Factory) da Task classe. Por exemplo: Task.Factory.StartNew(…).

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

  • Use as sobrecargas do método Task.ContinueWith. 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 tarefas para controlar melhor 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 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. 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 monitorizar 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 saia mais cedo caso um pedido de cancelamento chegue durante a renderização. Além disso, se o pedido de cancelamento chegar antes do início da renderização, deve impedir a operação de renderização:

internal static 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

Observação

Este exemplo utiliza Bitmap, que requer o pacote System.Drawing.Common e é suportado apenas em Windows. O padrão de tarefa intensiva em cálculo — usando Task.Run com um CancellationToken — aplica-se em todas as plataformas; deve-se substituir por uma biblioteca de imagem multiplataforma em alvos que não sejam do Windows.

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 no corpo de tal tarefa. Essa exceção contém o mesmo CancellationToken que é passado à tarefa, e esse token mostra que o cancelamento foi solicitado.

Se outra exceção não for tratada no corpo da tarefa, a tarefa termina no Faulted estado. Qualquer tentativa de aguardar pela tarefa ou aceder ao seu resultado provoca a abertura de uma exceção.

Tarefas ligadas a E/S

Para criar uma tarefa que não deva usar diretamente 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> . Controla o ciclo de vida desta tarefa usando TaskCompletionSource<TResult> métodos como SetResult, SetException, SetCanceled, e as suas TrySet variantes.

Suponha que quer criar uma tarefa que se conclua após um período de tempo especificado. Por exemplo, pode querer atrasar uma atividade na interface do utilizador. A System.Threading.Timer classe já oferece a capacidade de invocar assíncronamente um delegado após um período de tempo especificado. Ao usar TaskCompletionSource<TResult>, 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 este fim. Pode usá-lo dentro de outro método assíncrono, por exemplo, para implementar um ciclo 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 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 isso, use uma fonte com um dummy TResult (Boolean é uma boa escolha por defeito, mas se estiver preocupado que o utilizador do Task o faça baixar para um Task<TResult>, 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> DelaySimple(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 DelaySimple(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 limitadas por computação ou I/O. Podem representar uma mistura dos dois. Na verdade, muitas vezes combinas múltiplas operações assíncronas em operações mistas maiores. Por exemplo, o RenderAsync método num exemplo anterior realiza uma operação computacionalmente intensiva para renderizar uma imagem com base numa entrada imageData. Isso imageData pode vir de um serviço Web que você acessa de forma assíncrona:

public static 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

Observação

Este exemplo utiliza Bitmap, que requer o pacote System.Drawing.Common e é suportado apenas em Windows. O padrão de encadear um download assíncrono com uma operação assíncrona que exige muitos recursos computacionais aplica-se em todas as plataformas; utilize uma biblioteca de imagens multiplataforma para os alvos que não sejam do Windows.

Este exemplo também demonstra como um único token de cancelamento pode ser propagado 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.

Ver também