Uma tarefa representa uma conclusão
Uma Task permite acompanhar uma operação que pode terminar com sucesso, falha ou cancelamento. Task<int> acrescenta um resultado inteiro quando há sucesso; não é o próprio inteiro. Em var count = await pending, o tipo de count é int se pending for Task<int>.
A chamada de um método async começa a executar o seu corpo e pode chegar a um await cujo operando ainda está incompleto. Nessa situação, suspende esse método e devolve o controlo ao chamador. Se a tarefa já estiver concluída, await não precisa de suspender.
Este modelo não promete uma thread nova para cada operação nem torna código de CPU automaticamente paralelo. Também não protege dados partilhados entre chamadas. Ao ler um excerto, identifica que trabalho começou, de que resultado a instrução seguinte depende e que entidade observa a conclusão.
Devolver uma conclusão que o chamador possa observar
Uma biblioteca que grava dados deve permitir ao chamador saber quando a gravação acabou e se falhou. Um método async Task fornece esse ponto de observação; o chamador pode fazer await num try apropriado. Um método async void não oferece uma tarefa e é normalmente reservado a handlers de eventos cujo contrato exige void.
Envolver apenas a sua invocação num try/catch não permite tratar as falhas assíncronas desse método. Devolver true imediatamente depois de iniciar trabalho também não prova a conclusão. É possível iniciar trabalho e conservá-lo para observar depois, mas a aplicação precisa de definir o dono dessa tarefa e dos seus erros.
O nome Async é uma convenção útil; por si só não define o contrato nem garante que todo o corpo evite chamadas bloqueantes.
Combinar tarefas sem perder resultados e falhas
Task.WhenAll acompanha todas as tarefas fornecidas. Se first produzir A e second produzir B, await Task.WhenAll(first, second) fornece os resultados nessa ordem mesmo que second termine primeiro. Não é uma lista ordenada pela velocidade de resposta.
A tarefa combinada só termina depois de todas; se alguma falhar, fica Faulted. Sem falhas, algum cancelamento determina Canceled. Para construir um inventário de falhas, conserva a tarefa combinada e examina a sua Exception depois de confirmar Faulted.
Essa AggregateException contém as falhas retidas das tarefas fornecidas; uma única mensagem registada por um handler não demonstra que só houve uma falha. Não assumas também que WhenAll limita concorrência: fornecer muitas operações já iniciadas não cria automaticamente um limite de capacidade.
Cancelamento é um pedido cooperativo
O dono de um CancellationTokenSource pode pedir cancelamento; as operações recebem o respetivo token e decidem como responder segundo o contrato. Passar o token não prova que cada camada o observa. Num ciclo, um ponto explícito com ThrowIfCancellationRequested pode impedir o início do próximo item quando o pedido já está sinalizado.
Isso lança OperationCanceledException associada ao token. Uma API chamada deve receber o token se o seu contrato suportar esse mecanismo; criar uma origem nova e desligada não transmite o pedido anterior. Não confundas pedido com estado terminal: trabalho que ignora o pedido pode continuar.
E mesmo uma operação que termina por cancelamento pode ter produzido efeitos antes de parar. O dono deve gerir a origem e libertá-la quando o seu uso terminar, sem a eliminar enquanto outras partes ainda dependem dela.
Timeout da espera e resultado da operação
WaitAsync permite limitar uma espera por uma Task. Se o limite expirar, a tarefa que representa essa espera pode falhar com TimeoutException enquanto a tarefa original continua pendente. Esta diferença é essencial numa chamada remota: deixar de esperar não prova que o servidor deixou de trabalhar.
Para recuperar, conserva a identidade da operação e usa o protocolo da aplicação para consultar estado ou repetir de forma idempotente. A garantia de não duplicação tem de vir desse protocolo, não de await. Se o serviço não fornecer consulta ou identidade estável, explicita a incerteza em vez de marcar sucesso ou rollback sem evidência.
Também não elimines referências a trabalho ainda ativo sem definir quem o acompanha e quem conserva os recursos de que precisa.
O recurso tem de sobreviver ao trabalho
Um método normal que abre um leitor com using e devolve uma Task incompleta sai imediatamente do âmbito do leitor. Se essa tarefa ainda depende do leitor, encontra um recurso libertado demasiado cedo. Quando a função é dona do recurso, tornar o método async e fazer return await dentro do using conserva o âmbito até observar a conclusão.
Esta decisão também determina onde as exceções são tratadas. Uma variável local que apenas guarda a mesma Task não prolonga o âmbito. O raciocínio aplica-se igualmente a uma permissão de concorrência: só deve ser devolvida depois de terminar o trabalho que ela protege.
Se aplicares um timeout à espera e libertares logo o leitor ou a permissão, a operação original pode continuar a usar o recurso ou a consumir capacidade. Espera e operação precisam de contratos de tempo de vida coerentes.
Leitura guiada: adquirir, aguardar, libertar
O exemplo partilha uma única SemaphoreSlim com limite dois entre as chamadas da classe. O dono conserva-a durante o serviço; não é libertada enquanto há chamadas pendentes. WaitAsync(token) ocorre antes do try.
Se essa espera falhar por cancelamento antes de adquirir, não entra no finally e não devolve uma permissão inexistente. Depois de adquirir, o finally devolve exatamente uma permissão, quer work termine com sucesso, quer lance uma exceção. O delegado é válido, respeita o contrato de Task e representa todo o trabalho protegido; se iniciar tarefas independentes e devolver antes delas, o limite deixa de as abranger.
Cancelar é cooperativo: se work ignorar o token, mantém a permissão até terminar. O limite vale para esta instância partilhada, não para todos os processos do sistema. Nas avaliações, interpreta estes caminhos e seleciona respostas; não é necessária execução de código.
using System;
using System.Threading;
using System.Threading.Tasks;
public static class LimitedWork
{
private static readonly SemaphoreSlim Gate = new(2, 2);
public static async Task<int> RunAsync(
Func<CancellationToken, Task<int>> work,
CancellationToken token)
{
ArgumentNullException.ThrowIfNull(work);
await Gate.WaitAsync(token);
try
{
return await work(token);
}
finally
{
Gate.Release();
}
}
}Com duas chamadas ainda a executar work, uma terceira aguarda a permissão. Se a terceira for cancelada antes de adquirir, não chama Release.
Armadilhas comuns
Confundir Task com thread; expor async void em bibliotecas; tratar timeout como rollback; libertar permissões sem adquirir; fechar recursos enquanto tarefas ainda os utilizam.
Tópicos relacionados: Exceções, recursos e ficheiros · Métodos, parâmetros e padrões · Compilação, dependências e diagnóstico de execução
Acompanha a conclusão real e mantém recursos e permissões até terminar o trabalho que representam. Um pedido de cancelamento ou uma espera expirada não demonstra reversão.
Referência: Task asynchronous programming model · C# 14 / .NET 10; no preview features