Introdução e visão geral do TAP (padrão assíncrono baseado em tarefas) no .NET

No .NET, o padrão assíncrono baseado em tarefas é o padrão de design assíncrono recomendado para novos desenvolvimentos. Ele é baseado nos tipos Task e Task<TResult> no namespace System.Threading.Tasks, que são usados para representar operações assíncronas.

Nomenclatura, parâmetros e tipos de retorno

O TAP usa um único método para representar o início e a conclusão de uma operação assíncrona. Isso contrasta com o Modelo de programação assíncrona (APM ou IAsyncResult) e o Padrão assíncrono baseado em eventos (EAP). O APM requer os métodos Begin e End. O EAP requer um método que tenha o sufixo Async e também requer um ou mais eventos, tipos delegados de manipulador de eventos e tipos derivados de EventArg. Os métodos assíncronos no TAP incluem o sufixo Async após o nome da operação para os métodos que retornam tipos aguardáveis, como Task, Task<TResult>, ValueTask e ValueTask<TResult>. Por exemplo, uma operação Get assíncrona que retorna um Task<String> pode ser nomeada GetAsync. Se você estiver adicionando um método TAP a uma classe que já contenha o nome do método EAP com o sufixo Async, use, em vez dele, o sufixo TaskAsync. Por exemplo, se a classe já possuir um método GetAsync, use o nome GetTaskAsync. Se um método inicia com uma operação assíncrona, mas não retorna um tipo aguardável, o nome dele deve começar com Begin, Start ou algum outro verbo que sugira que esse método não retorna nem gera o resultado da operação.  

Um método TAP retorna System.Threading.Tasks.Task ou System.Threading.Tasks.Task<TResult>, dependendo se o método síncrono correspondente retorna void ou um tipo TResult.

Os parâmetros de um método TAP devem corresponder aos parâmetros de suas contrapartes síncronas e devem ser fornecidos na mesma ordem. No entanto, os parâmetros out e ref são isentos dessa regra e devem ser evitados inteiramente. Quaisquer dados retornados por um parâmetro out ou ref deve, em vez disso, ser retornado como parte do TResult retornado por Task<TResult> e deve usar uma tupla ou uma estrutura de dados personalizada para acomodar diversos valores. Considere também adicionar um parâmetro CancellationToken mesmo se a contraparte síncrona do método TAP não oferecer nenhum.

Os métodos que são dedicados exclusivamente à criação, ao tratamento ou à combinação de tarefas (onde a intenção assíncrona do método está clara no nome do método ou no nome do tipo ao qual o método pertence) não precisam seguir esse padrão de nomenclatura; esses métodos são geralmente denominados combinadores. Os exemplos de combinadores incluem WhenAll e WhenAny e são discutidos na seção Usando os combinadores baseados em tarefas internos do artigo Consumindo o padrão assíncrono baseado em tarefa.

Para obter exemplos de como a sintaxe do TAP difere da sintaxe usada em padrões de programação assíncronos herdados, como APM (Asynchronous Programming Model) e EAP (Event-based Asynchronous Pattern), confira Padrões de programação assíncrona .

Como iniciar uma operação assíncrona

Um método assíncrono baseado no TAP pode fazer uma pequena quantidade de trabalho de forma síncrona, como validar argumentos e iniciar a operação assíncrona, antes de retornar a tarefa resultante. O trabalho síncrono deve ser mantido no mínimo possível para que o método assíncrono possa retornar rapidamente. Os motivos para um retorno rápido incluem o seguinte:

  • Os métodos assíncronos podem ser chamados de segmentos de interface do usuário (UI), e qualquer trabalho síncrono longo pode prejudicar a resposta do aplicativo.

  • É possível iniciar vários métodos assíncronos simultaneamente. Portanto, qualquer trabalho longo na parte síncrona de um método assíncrono pode atrasar a inicialização de outras operações assíncronas, diminuindo assim os benefícios de simultaneidade.

Em alguns casos, a quantidade de trabalho necessária para concluir a operação é menor do que a quantidade de trabalho necessária para iniciar a operação de forma assíncrona. Ler de um fluxo em que a operação de leitura pode ser satisfeita pelos dados que já estão armazenados no buffer de memória é um exemplo de cenário desse tipo. Em casos como esse, a operação pode ser concluída de forma síncrona e retornar uma tarefa que já havia sido concluída.

Exceções

Um método assíncrono deve gerar uma exceção para ser lançada fora da chamada do método assíncrono somente em resposta a um erro de uso. Erros de uso nunca devem ocorrer em código de produção. Por exemplo, se a transmissão de uma referência nula (Nothing no Visual Basic) como um dos argumentos do método resultar em um estado de erro (geralmente representado por uma exceção ArgumentNullException), será possível modificar o código de chamada para garantir que uma referência nula nunca seja transmitida. Para todos outros erros, as exceções que ocorrem quando um método assíncrono está em execução devem ser atribuídas a tarefa retornada, mesmo quando o método assíncrono é concluído de forma síncrona antes de a tarefa ser retornada. Normalmente, uma tarefa contém no máximo uma exceção. No entanto, se a tarefa representa várias operações (por exemplo, WhenAll), várias exceções podem ser associadas a uma única tarefa.

Ambiente de destino

Quando você implementa um método TAP, pode determinar o local em que a execução assíncrona ocorre. É possível optar por executar a carga de trabalho no pool de threads, implementá-la usando E/S assíncrona (sem vinculação a um thread para a maior parte da execução da operação), executá-la em um thread específico (como o thread da IU) ou usar qualquer número de contextos potenciais. Um método TAP pode até não ter nada a executar e pode retornar apenas um Task, que representa a ocorrência de uma condição em outro lugar no sistema (por exemplo, uma tarefa que representa os dados que chegam a uma estrutura de dados na fila).

O chamador do método TAP pode bloquear a espera para o método TAP ser concluído com a espera síncrona na tarefa resultante ou pode executar código adicional (continuação) quando a operação assíncrona é concluída. O criador do código de continuação tem o controle sobre o local em que o código é executado. Você pode criar o código de continuação explicitamente com os métodos da classe Task (por exemplo, ContinueWith) ou implicitamente usando o suporte à linguagem compilado nas continuações (por exemplo, await em C#, Await em Visual Basic, AwaitValue em F#).

Status da tarefa

A classe Task fornece um ciclo de vida para operações assíncronas, e esse ciclo é representado pela enumeração TaskStatus. Para dar suporte a casos de canto de tipos derivados de Task e Task<TResult> e à separação da construção do agendamento, a classe Task expõe um método Start. As tarefas que são criadas pelos construtores públicos Task são chamadas de tarefas frias, porque começam seu ciclo de vida no estado não agendado Created e são agendadas somente quando Start é chamado nessas instâncias.

Todas tarefas restantes começam seu ciclo de vida em um estado quente, o que significa que as operações assíncronas que elas representam já foram iniciadas, e seus status de tarefa são um valor de enumeração diferente de TaskStatus.Created. Todas as tarefas que são retornadas dos métodos TAP devem ser ativadas. Se um método TAP usar internamente o construtor de uma tarefa para criar uma instância da tarefa a ser retornada, o método TAP deverá chamar Start no objeto Task antes de retorná-la. Os consumidores de um método TAP podem presumir com segurança que a tarefa retornada está ativa e não devem tentar chamar Start em qualquer Task que é retornado de um método TAP. A chamada Start em uma tarefa ativa resulta em uma exceção de InvalidOperationException.

Cancelamento (opcional)

Em TAP, o cancelamento é opcional para implementadores e consumidores de métodos assíncronos. Se uma operação permite o cancelamento, ela expõe uma sobrecarga do método assíncrono que aceita um símbolo de cancelamento (CancellationToken). Por convenção, o parâmetro é chamado cancellationToken.

public Task ReadAsync(byte [] buffer, int offset, int count,
                      CancellationToken cancellationToken)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          cancellationToken As CancellationToken) _
                          As Task

A operação assíncrona monitora esse token para solicitações de cancelamento. Se ela recebe uma solicitação de cancelamento, pode escolher honrar a solicitação e cancelar a operação. Se a solicitação de cancelamento resulta em algum trabalho ser encerrado prematuramente, o método TAP retorna uma tarefa que termina em estado de Canceled; não há nenhum resultado disponível, e nenhuma exceção é gerada. O estado Canceled é considerado um estado final (concluído) para uma tarefa, juntamente com os estados Faulted e RanToCompletion. Portanto, se uma tarefa está no estado Canceled, sua propriedade IsCompleted retorna true. Quando uma tarefa termina no estado Canceled, todas as continuações registradas com a tarefa ou agendadas são executadas, a menos que uma opção de continuação como NotOnCanceled tenha sido especificada para optar pela não continuação. Qualquer código que esteja aguardando de forma assíncrona uma tarefa cancelada com o uso de recursos de linguagem continuará a ser executada, mas receberá OperationCanceledException ou uma exceção derivada dela. O código bloqueado que espera de forma síncrona a tarefa através de métodos como Wait e WaitAll também continuará a ser executado com uma exceção.

Se um token de cancelamento solicitou o cancelamento antes do método TAP que aceita esse token ter sido chamado, o método TAP deve retornar uma tarefa Canceled. No entanto, se o cancelamento é solicitado enquanto a operação assíncrona é executada, a operação assíncrona não precisa aceitar a solicitação de cancelamento. A tarefa retornada deverá terminar no estado Canceled somente se a operação terminar como um resultado da solicitação de cancelamento. Se o cancelamento for solicitado, mas um resultado ou uma exceção ainda forem gerados, a tarefa deve terminar no estado RanToCompletion ou Faulted.

Para métodos assíncronos que expõem a capacidade de cancelamento em primeiro lugar, não é necessário fornecer uma sobrecarga que não aceite um token de cancelamento. Para os métodos que não podem ser cancelados, não forneça sobrecargas que aceitem um token de cancelamento; isso ajuda a indicar ao chamador se o método de destino é realmente cancelável. O código consumidor que não deseja cancelamento pode chamar um método que aceita um CancellationToken e fornece None como o valor do argumento. None é funcionalmente equivalente ao CancellationToken padrão.

Relatório de progresso (opcional)

Algumas operações assíncronas beneficiam-se do fornecimento de notificações de progresso; esses são normalmente usados para atualizar uma interface do usuário com informações sobre o andamento da operação assíncrona.

No TAP, o progresso é tratado por uma interface IProgress<T> que é passada para o método assíncrono como um parâmetro que é chamado geralmente de progress. Fornecer a interface de progresso quando o método assíncrono é chamado ajuda a eliminar as condições de corrida resultantes do uso incorreto (isto é, quando os manipuladores de eventos registrados incorretamente depois que a operação é iniciada podem carecer de atualizações). Mais importante, a interface de progresso suporta implementações de variação de progresso, conforme determinado pelo código consumidor. Por exemplo, o código consumidor pode se preocupar somente com a atualização de progresso mais recente, armazenar em buffer todas as atualizações, invocar uma ação para cada atualização ou controlar se a invocação passou por marshalling para um segmento específico. Todas essas opções são possíveis usando uma implementação diferente da interface, personalizada para as necessidades específicas do consumidor. Assim como no cancelamento, as implementações de TAP devem fornecer um parâmetro de IProgress<T> somente se a API oferecer suporte a notificações de progresso.

Por exemplo, se o método ReadAsync discutido anteriormente nesse artigo for capaz de relatar o progresso intermediário na forma do número de bytes lidos até aqui, o retorno de chamada de progresso poderá ser uma interface IProgress<T>:

public Task ReadAsync(byte[] buffer, int offset, int count,
                      IProgress<long> progress)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          progress As IProgress(Of Long)) As Task

Se um método FindFilesAsync retornar uma lista de todos os arquivos que atendem a um padrão de pesquisa específico, o retorno de chamada de progresso poderá fornecer uma estimativa da porcentagem de trabalho concluído e o conjunto atual de resultados parciais. Ele pode fornecer essas informações com uma tupla:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
            string pattern,
            IProgress<Tuple<double,
            ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

ou com um tipo de dados específico da API:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of FindFilesProgressInfo)) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

No último caso, o tipo de dados especial geralmente é sufixado com ProgressInfo.

Se as implementações do TAP fornecerem sobrecargas que aceitam um parâmetro progress, elas deverão permitir que o argumento seja null, caso em que nenhum progresso é relatado. As implementações do TAP devem relatar o progresso para o objeto Progress<T> de maneira síncrona, o que permite que o método assíncrono forneça o progresso rapidamente. Isso também permite que o consumidor do progresso determine como e onde é melhor lidar com a informação. Por exemplo, a instância de progresso poderia optar por controlar retornos de chamada e gerar eventos em um contexto de sincronização capturado.

Implementações de IProgress<T>

O .NET fornece a classe Progress<T>, que implementa IProgress<T>. A classe Progress<T> é declarada da seguinte forma:

public class Progress<T> : IProgress<T>  
{  
    public Progress();  
    public Progress(Action<T> handler);  
    protected virtual void OnReport(T value);  
    public event EventHandler<T>? ProgressChanged;  
}  

Uma instância de Progress<T> expõe um evento ProgressChanged, o qual é gerado sempre que a operação assíncrona relata uma atualização de progresso. O evento ProgressChanged é gerado no objeto de SynchronizationContext que foi capturado quando a instância de Progress<T> foi criada. Se não havia contexto de sincronização disponível, um contexto padrão que tem como alvo o pool de segmentos é usado. É possível registrar manipuladores com esse evento. Um único manipulador também pode ser fornecido para o construtor Progress<T> para maior conveniência. Seu comportamento é exatamente como um manipulador de eventos para o evento ProgressChanged. As atualizações de progresso são geradas de forma assíncrona para evitar atrasar a operação assíncrona enquanto os manipuladores de eventos são executados. Outra implementação de IProgress<T> pode optar por aplicar uma semântica diferente.

Escolhendo as sobrecargas a serem fornecidas

Se uma implementação de TAP ambos os parâmetros CancellationToken e IProgress<T> opcionais, ela poderá potencialmente exigir até quatro cargas de trabalho:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

No entanto, como muitas implementações do TAP não fornecem recursos de cancelamento ou progresso, elas exigem um único método:

public Task MethodNameAsync(…);  
Public MethodNameAsync(…) As Task  

Se uma implementação de TAP oferecer suporte a cancelamento ou progresso, mas não a ambos, ela poderá fornecer duas sobrecargas:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
  
// … or …  
  
public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task  
  
' … or …  
  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task  

Se uma implementação de TAP oferecer suporte a cancelamento e progresso, ela poderá expor todas as quatro sobrecargas. No entanto, ela pode fornecer somente estas duas:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Para compensar as duas combinações intermediárias ausentes, os desenvolvedores podem passar None ou um CancellationToken padrão para o parâmetro cancellationToken e null para o parâmetro progress.

Para que todo uso do método TAP dê suporte a cancelamentos ou progressos, omita as sobrecargas que não aceitam o parâmetro relevante.

Para expor diversas sobrecargas a fim de tornar o cancelamento ou o progresso opcional, as sobrecargas que não dão suporte ao cancelamento ou ao progresso devem se comportar como se tivessem transmitido None para cancelamento ou null para progresso à sobrecarga que dá suporte a eles.

Título Descrição
Padrões de programação assíncrona Apresenta os três padrões para execução de operações assíncronas: TAP (Padrão Assíncrono Baseado em Tarefas), APM (Modelo de Programação Assíncrona) e EAP (Padrão Assíncrono Baseado em Eventos).
Implementando o padrão assíncrono baseado em tarefa Descreve como implementar o TAP de três formas: usando os compiladores C# e Visual Basic no Visual Studio, manualmente ou por meio de uma combinação de compilador com o método manual.
Consumindo o padrão assíncrono baseado em tarefa Descreve como você pode usar tarefas e retornos de chamada para implementar a espera sem causar bloqueios.
Interoperabilidade com outros tipos e padrões assíncronos Descreve como usar o TAP (Padrão Assíncrono Baseado em Tarefas) para implementar o APM (Modelo de Programação Assíncrona) e o EAP (Padrão Assíncrono Baseado em Eventos).