Padrão assíncrono baseado em tarefas (TAP) no .NET: introdução e visão geral

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

Nomenclatura, parâmetros e tipos de retorno

A 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 padrão APM ( IAsyncResultAsynchronous Programming Model) e o EAP (Event-based Asynchronous Pattern). APM requer Begin e End métodos. O EAP requer um método que tenha o sufixo Async e também requer um ou mais eventos, tipos delegados do manipulador de eventos e EventArgtipos derivados. Os métodos assíncronos em TAP incluem o sufixo Async após o nome da operação para métodos que retornam tipos aguardados, como Task, Task<TResult>, ValueTaske ValueTask<TResult>. Por exemplo, uma operação assíncrona Get que retorna um Task<String> pode ser nomeada GetAsync. Se você estiver adicionando um método TAP a uma classe que já contém um nome de método EAP com o sufixo Async , use o sufixo TaskAsync . Por exemplo, se a classe já tiver um GetAsync método, use o nome GetTaskAsync. Se um método inicia uma operação assíncrona, mas não retorna um tipo aguardado, seu nome deve começar com Begin, Start, ou algum outro verbo para sugerir que esse método não retorna ou lança o resultado da operação.  

Um método TAP retorna a System.Threading.Tasks.Task ou a System.Threading.Tasks.Task<TResult>, com base no fato de o método síncrono correspondente retornar void ou um tipo TResult.

Os parâmetros de um método TAP devem corresponder aos parâmetros do seu homólogo síncrono e devem ser fornecidos pela mesma ordem. No entanto, out e ref os parâmetros estão isentos desta regra e devem ser totalmente evitados. Todos os dados que teriam sido retornados por meio de um out parâmetro ou ref devem, em vez disso, ser retornados como parte do retornado pelo Task<TResult>, e devem usar uma tupla TResult ou uma estrutura de dados personalizada para acomodar vários valores. Além disso, considere adicionar um CancellationToken parâmetro mesmo que a contraparte síncrona do método TAP não ofereça um.

Os métodos que são dedicados exclusivamente à criação, manipulação ou combinação de tarefas (onde a intenção assíncrona do método é 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; tais métodos são muitas vezes referidos como combinadores. Exemplos de combinadores incluem WhenAll e WhenAny, e são discutidos na seção Usando os combinadores baseados em tarefas internas do artigo Consumindo o padrão assíncrono baseado em tarefas.

Para obter exemplos de como a sintaxe TAP difere da sintaxe usada em padrões de programação assíncrona herdados, como o Modelo de Programação Assíncrona (APM) e o Padrão Assíncrono Baseado em Evento (EAP), consulte Padrões de programação assíncrona.

Iniciando uma operação assíncrona

Um método assíncrono baseado em 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 ao mínimo para que o método assíncrono possa retornar rapidamente. As razões para um retorno rápido incluem:

  • Métodos assíncronos podem ser invocados a partir de threads de interface do usuário (UI) e qualquer trabalho síncrono de longa execução pode prejudicar a capacidade de resposta do aplicativo.

  • Vários métodos assíncronos podem ser iniciados simultaneamente. Portanto, qualquer trabalho de longa execução na parte síncrona de um método assíncrono pode atrasar o início de outras operações assíncronas, diminuindo assim os benefícios da 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. A leitura de um fluxo onde a operação de leitura pode ser satisfeita por dados que já estão armazenados em buffer na memória é um exemplo desse cenário. Nesses casos, a operação pode ser concluída de forma síncrona e pode retornar uma tarefa que já foi concluída.

Exceções

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

Ambiente de destino

Ao implementar um método TAP, você pode determinar onde a execução assíncrona ocorre. Você pode optar por executar a carga de trabalho no pool de threads, implementá-la usando E/S assíncrona (sem estar vinculado a um thread na maior parte da execução da operação), executá-la em um thread específico (como o thread da interface do usuário) ou usar qualquer número de contextos potenciais. Um método TAP pode até não ter nada para executar e pode apenas retornar um Task que representa a ocorrência de uma condição em outro lugar do sistema (por exemplo, uma tarefa que representa dados que chegam a uma estrutura de dados enfileirada).

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

Estado da tarefa

A Task classe fornece um ciclo de vida para operações assíncronas TaskStatus , e esse ciclo é representado pela enumeração. Para suportar casos de canto de tipos que derivam de Task e Task<TResult>, e para apoiar a separação da construção do agendamento, a Task classe expõe um Start método. As tarefas 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 são chamadas nessas instâncias.

Todas as outras tarefas começam seu ciclo de vida em um estado quente, o que significa que as operações assíncronas que representam já foram iniciadas e seu status de tarefa é um valor de enumeração diferente de TaskStatus.Created. Todas as tarefas retornadas dos métodos TAP devem ser ativadas. Se um método TAP usa internamente o construtor de uma tarefa para instanciar a tarefa a ser retornada, o método TAP deve chamar Start o Task objeto antes de retorná-lo. 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 qualquer Task um que seja retornado de um método TAP. Chamar Start uma tarefa ativa resulta em uma InvalidOperationException exceção.

Cancelamento (opcional)

Na TAP, o cancelamento é opcional para implementadores de método assíncrono e consumidores de método assíncrono. Se uma operação permitir o cancelamento, ela expõe uma sobrecarga do método assíncrono que aceita um token de cancelamento (CancellationToken instância). 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 receber um pedido de cancelamento, pode optar por honrar esse pedido e cancelar a operação. Se o pedido de cancelamento resultar no término prematuro do trabalho, o método TAP retorna uma tarefa que termina no Canceled estado, não há resultado disponível e nenhuma exceção é lançada. O Canceled estado é considerado um estado final (concluído) para uma tarefa, juntamente com os Faulted estados e RanToCompletion . Portanto, se uma tarefa estiver no Canceled estado, sua IsCompleted propriedade retornará true. Quando uma tarefa é concluída no Canceled estado, todas as continuações registradas com a tarefa são agendadas ou executadas, a menos que uma opção de continuação, como NotOnCanceled foi especificada, desative a continuação. Qualquer código que esteja aguardando assincronamente por uma tarefa cancelada por meio do uso de recursos de linguagem continua a ser executado, mas recebe uma OperationCanceledException ou uma exceção derivada dele. Código bloqueado aguardando síncrona na tarefa por meio de métodos como Wait e WaitAll também continuar a ser executado com uma exceção.

Se um token de cancelamento tiver solicitado cancelamento antes que o método TAP que aceita esse token seja chamado, o método TAP deverá retornar uma Canceled tarefa. No entanto, se o cancelamento for solicitado enquanto a operação assíncrona estiver em execução, a operação assíncrona não precisará aceitar a solicitação de cancelamento. A tarefa retornada deve terminar no Canceled estado somente se a operação terminar como resultado da solicitação de cancelamento. Se o RanToCompletion cancelamento for solicitado, mas um resultado ou uma exceção ainda for produzida, a tarefa deve terminar no estado ou Faulted .

Para métodos assíncronos que desejam expor a capacidade de ser cancelado em primeiro lugar, você não precisa fornecer uma sobrecarga que não aceite um token de cancelamento. Para 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 do consumidor que não deseja cancelamento pode chamar um método que aceita um CancellationToken e fornecer None como o valor do argumento. None é funcionalmente equivalente ao padrão CancellationToken.

Relatório de progresso (opcional)

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

No TAP, o progresso é tratado através de uma IProgress<T> interface, que é passada para o método assíncrono como um parâmetro que geralmente é chamado progressde . Fornecer a interface de progresso quando o método assíncrono é chamado ajuda a eliminar as condições de corrida resultantes do uso incorreto (ou seja, quando manipuladores de eventos registrados incorretamente após o início da operação podem perder atualizações). Mais importante ainda, a interface de progresso suporta implementações variadas de progresso, conforme determinado pelo código consumidor. Por exemplo, o código de consumo pode se preocupar apenas com a atualização de progresso mais recente, ou pode querer armazenar em buffer todas as atualizações, ou pode querer invocar uma ação para cada atualização, ou pode querer controlar se a invocação é empacotada para um thread específico. Todas estas opções podem ser alcançadas usando uma implementação diferente da interface, personalizada para as necessidades específicas do consumidor. Tal como acontece com o cancelamento, as implementações TAP devem fornecer um IProgress<T> parâmetro apenas se a API suportar notificações de progresso.

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

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 FindFilesAsync método 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 do conjunto atual de resultados parciais. Poderia fornecer esta informação 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))

Neste último caso, o tipo de dados especial é geralmente sufixo com ProgressInfo.

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

Implementações IProgress<T>

O .NET fornece a classe, que implementa IProgress<T>o Progress<T> . A Progress<T> classe é 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 expõe um ProgressChanged evento, que é gerado sempre que a operação assíncrona relata uma atualização de Progress<T> progresso. O ProgressChanged evento é gerado no objeto que foi capturado SynchronizationContext quando a Progress<T> instância foi instanciada. Se nenhum contexto de sincronização estiver disponível, um contexto padrão direcionado ao pool de threads será usado. Os manipuladores podem ser registrados com este evento. Um único manipulador também pode ser fornecido ao Progress<T> construtor por conveniência e se comporta como um manipulador de eventos para o ProgressChanged evento. 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 estão em execução. Outra IProgress<T> implementação poderia optar por aplicar semânticas diferentes.

Escolhendo as sobrecargas a fornecer

Se uma implementação TAP usar os parâmetros opcionais CancellationToken e opcionais IProgress<T> , ela poderá exigir até quatro sobrecargas:

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, muitas implementações TAP não fornecem recursos de cancelamento ou progresso, por isso exigem um único método:

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

Se uma implementação TAP suportar cancelamento ou progresso, mas não ambos, pode 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 TAP suportar cancelamento e progresso, pode expor todas as quatro sobrecargas. No entanto, só pode prever os dois seguintes:

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 padrão CancellationToken para o cancellationToken parâmetro e null para o progress parâmetro.

Se você espera que cada uso do método TAP suporte cancelamento ou progresso, você pode omitir as sobrecargas que não aceitam o parâmetro relevante.

Se você decidir expor várias sobrecargas para tornar o cancelamento ou o progresso opcionais, as sobrecargas que não suportam cancelamento ou progresso devem se comportar como se tivessem passado None por cancelamento ou null por progresso para a sobrecarga que as suporta.

Title Description
Padrões de programação assíncrona Apresenta os três padrões para executar operações assíncronas: o padrão assíncrono baseado em tarefas (TAP), o modelo de programação assíncrona (APM) e o padrão assíncrono baseado em eventos (EAP).
Implementando o padrão assíncrono baseado em tarefas Descreve como 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 através de uma combinação do compilador e métodos manuais.
Consumindo o padrão assíncrono baseado em tarefas Descreve como você pode usar tarefas e retornos de chamada para realizar a espera sem bloquear.
Interoperabilidade com outros padrões e tipos assíncronos Descreve como usar o padrão assíncrono baseado em tarefas (TAP) para implementar o modelo de programação assíncrona (APM) e o padrão assíncrono baseado em evento (EAP).