Partilhar via


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 nos tipos Task e Task<TResult> no espaço de nomes System.Threading.Tasks, 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 Modelo de Programação Assíncrono (APM ou IAsyncResult) e o Padrão Assíncrono Baseado em Eventos (EAP). 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 os tipos derivados de EventArg. 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 System.Threading.Tasks.Task ou System.Threading.Tasks.Task<TResult>, com base em 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 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 através de um parâmetro out ou ref devem, em vez disso, ser incluídos como parte do TResult retornado pelo Task<TResult>, utilizando uma tupla 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, e esse ciclo é representado pela enumeração TaskStatus. Para suportar casos extremos de tipos que derivam de Task e Task<TResult>, e para apoiar a separação da construção do agendamento, a classe Task expõe um método Start. 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 assumir com segurança que a tarefa retornada está ativa e não devem tentar chamar Start em qualquer Task que seja retornado de um método TAP. Chamar Start em uma tarefa ativa resulta numa InvalidOperationException excepçã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 estado Canceled; não há resultado disponível e não é lançada nenhuma exceção. 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 estado Canceled, todas as continuações registadas com a tarefa são agendadas ou executadas, a menos que uma opção de continuação, como a NotOnCanceled, tenha sido especificada para não continuar. 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 que é bloqueado, aguardando de forma síncrona na tarefa através de métodos como Wait e WaitAll, continua 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 cancelamento for solicitado, mas ainda for produzido um resultado ou uma exceção, a tarefa deve terminar no estado RanToCompletion ou Faulted.

Para métodos assíncronos que desejam oferecer a possibilidade de serem cancelados prioritariamente, não é necessário dar 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 é gerido através de uma IProgress<T> interface, que é passada para o método assíncrono como um parâmetro geralmente chamado progress. Fornecer a interface de progresso no momento em que o método assíncrono é chamado ajuda a evitar condições de corrida que resultam de uso incorreto. Isso acontece quando os manipuladores de eventos, que são registrados de forma incorreta após o início da operação, podem falhar em receber 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 consumidor 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 é encaminhada 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 devolver uma lista de todos os arquivos que atendem a um padrão de pesquisa específico, o callback de progresso poderá fornecer uma estimativa da porcentagem de trabalho concluído e o conjunto atual de resultados parciais. Poderia fornecer esta informação com uma tupla, por exemplo:

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 sufixado 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 Progress<T> de forma síncrona, o 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 poderá optar por organizar chamadas de retorno e gerar eventos num contexto de sincronização capturado.

Implementações 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 de ProgressChanged, que é acionado sempre que a operação assíncrona relata uma atualização de progresso. O ProgressChanged evento é gerado no SynchronizationContext objeto que foi capturado quando a instância Progress<T> 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.

Escolha das sobrecargas a serem fornecidas

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 ao mesmo tempo, 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 TAP suportar tanto o cancelamento como a progressão, poderá 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.

Caso decida expor várias sobrecargas para tornar o cancelamento ou o progresso opcionais, as sobrecargas que não suportam cancelamento ou progresso devem comportar-se como se tivessem passado None por cancelamento ou null por progresso para a sobrecarga que suporte essas funcionalidades.

Título Descrição
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).