C#: Programação assíncrona
1. Síncrono x Assíncrono
O código síncrono é executado linha por linha, em ordem. Quando encontra uma operação demorada, ele bloqueia a thread atual, fazendo com que o programa pare de responder. O código assíncrono libera a thread enquanto aguarda operações demoradas, permitindo que o programa continue realizando outras tarefas.
Problemas com o bloqueio síncrono:
- A thread da interface do usuário está ocupada, fazendo com que a interface congele e deixe de responder
- As threads do servidor ficam ociosas aguardando operações de E/S, o que reduz a taxa de processamento
- Os usuários enfrentam tempos de espera mais longos, o que resulta em uma experiência insatisfatória
Vantagens da comunicação assíncrona:
- Operações com uso intensivo de E/S (rede, arquivos, banco de dados) não bloqueiam threads
- Operações que exigem muito da CPU podem ser transferidas para o pool de threads, mantendo a interface do usuário responsiva
- Melhoria na capacidade de processamento simultâneo e no rendimento das aplicações
▶ Exemplo
using System;
using System.Threading;
Console.WriteLine("Sync start: " + DateTime.Now.ToString("HH:mm:ss"));
Thread.Sleep(2000);
Console.WriteLine("Sync end: " + DateTime.Now.ToString("HH:mm:ss"));
Sync start: 10:00:00
Sync end: 10:00:02
2. Tarefa e Tarefa<T>
Task representa uma operação assíncrona que não retorna um resultado. Task<T> representa uma operação assíncrona que retorna um resultado do tipo T. Esses são os tipos fundamentais da programação assíncrona em C#.
| Tipo | Descrição | Exemplo |
|---|---|---|
Task |
Operação assíncrona sem valor de retorno | Task.Delay(1000) |
Task<T> |
Operação assíncrona com valor de retorno | Task.FromResult(42) |
▶ Exemplo
using System;
using System.Threading.Tasks;
Task noResultTask = Task.Delay(500);
Task```<int>``` resultTask = Task.FromResult(42);
await noResultTask;
int value = await resultTask;
Console.WriteLine("Retrieved value: " + value);
Retrieved value: 42
3. Sintaxe de async/await
O modificador async marca um método como assíncrono, e o operador await aguarda a conclusão de uma operação assíncrona sem bloquear a thread. O await só pode ser usado dentro de um método async.
Regras de sintaxe:
- Os métodos
asyncretornamTask,Task<T>ouValueTask<T> awaitsuspende a execução do método, aguarda a conclusão da tarefa e, em seguida, continua- Um método
asyncretorna ao chamador no primeiroawait
▶ Exemplo
using System;
using System.Threading.Tasks;
async Task DoWorkAsync()
{
Console.WriteLine("Work start: " + DateTime.Now.ToString("HH:mm:ss.fff"));
await Task.Delay(1000);
Console.WriteLine("Work end: " + DateTime.Now.ToString("HH:mm:ss.fff"));
}
async Task```<string>``` GetMessageAsync()
{
await Task.Delay(500);
return "Async message arrived";
}
await DoWorkAsync();
string msg = await GetMessageAsync();
Console.WriteLine(msg);
Work start: 10:00:00.000
Work end: 10:00:01.000
Async message arrived
4. Task.Run
Task.Run transfere tarefas que exigem muito da CPU para o pool de threads para execução, evitando o bloqueio da thread atual. Adequado para operações que exigem muito processamento, mas não para operações de E/S.
▶ Exemplo
using System;
using System.Threading.Tasks;
int ComputeHeavy()
{
int result = 0;
for (int i = 0; i < 100_000_000; i++)
{
result += i;
}
return result;
}
Console.WriteLine("Starting computation...");
int total = await Task.Run(() => ComputeHeavy());
Console.WriteLine("Computation result: " + total);
Starting computation...
Computation result: 4999999950000000
5. Task.WhenAll / WhenAny / Delay
| Método | Descrição |
|---|---|
Task.WhenAll |
Aguarda a conclusão de todas as tarefas |
Task.WhenAny |
Aguarda a conclusão de qualquer uma das tarefas |
Task.Delay |
Aguarda de forma assíncrona pelo número especificado de milissegundos, substituindo Thread.Sleep |
▶ Exemplo
using System;
using System.Threading.Tasks;
async Task```<string>``` FetchAsync(string site)
{
await Task.Delay(new Random().Next(500, 1500));
return site + " loaded";
}
Task```<string>``` t1 = FetchAsync("SiteA");
Task```<string>``` t2 = FetchAsync("SiteB");
Task```<string>``` t3 = FetchAsync("SiteC");
string[] all = await Task.WhenAll(t1, t2, t3);
Console.WriteLine("--- WhenAll complete ---");
foreach (string s in all)
{
Console.WriteLine(s);
}
Task```<string>``` first = await Task.WhenAny(t1, t2, t3);
Console.WriteLine("First completed: " + first.Result);
--- WhenAll complete ---
SiteA loaded
SiteB loaded
SiteC loaded
First completed: SiteA loaded
6. Convenção de nomenclatura para métodos assíncronos
Os nomes dos métodos assíncronos devem usar o sufixo Async. Essa é uma convenção do .NET que facilita a distinção entre métodos síncronos e assíncronos.
Regras:
- Os métodos que retornam
TaskouTask<T>devem ter o sufixoAsync - por exemplo,
Read()→ReadAsync(),GetUser()→GetUserAsync() - Os manipuladores de eventos são excluídos (por exemplo,
Button_Click)
▶ Exemplo
using System;
using System.Threading.Tasks;
async Task```<string>``` ReadFileAsync(string path)
{
await Task.Delay(200);
return "Read: " + path;
}
async Task SaveDataAsync(string data)
{
await Task.Delay(100);
Console.WriteLine("Saved: " + data);
}
string content = await ReadFileAsync("/data.txt");
await SaveDataAsync(content);
Saved: Read: /data.txt
7. Mecanismo de cancelamento do CancellationToken
CancellationToken é utilizado para o cancelamento cooperativo de operações assíncronas. O chamador envia um sinal de cancelamento por meio de CancellationTokenSource, e o chamado verifica o token e encerra a operação antecipadamente.
Instruções de uso:
- Criar um
CancellationTokenSource - Passe
cts.Tokenpara o método assíncrono - O método assíncrono verifica periodicamente
token.IsCancellationRequestedou o passa para APIs canceláveis - Ligue para
cts.Cancel()caso seja necessário cancelar
▶ Exemplo
using System;
using System.Threading;
using System.Threading.Tasks;
async Task LongWorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
Console.WriteLine("Step " + (i + 1));
await Task.Delay(300, token);
}
Console.WriteLine("All done");
}
using var cts = new CancellationTokenSource();
cts.CancelAfter(1200);
try
{
await LongWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation cancelled");
}
Step 1
Step 2
Step 3
Step 4
Operation cancelled
8. IAsyncEnumerable<T> Fluxos assíncronos
O C# 8 introduziu o IAsyncEnumerable<T>, que oferece suporte à geração e ao consumo assíncronos de fluxos de dados. Use o await foreach para iterar itens de forma assíncrona, o que é adequado para cenários como leituras em lote de bancos de dados e respostas de rede em streaming.
▶ Exemplo
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
async IAsyncEnumerable```<int>``` GenerateNumbersAsync()
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(200);
yield return i;
}
}
await foreach (int num in GenerateNumbersAsync())
{
Console.WriteLine("Received: " + num);
}
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
9. Armadilhas comuns na programação assíncrona
(1) ⚠️ Evite funções assíncronas com retorno void
async void não pode ser awaitado, as exceções não podem ser capturadas e isso pode causar falhas no aplicativo. Utilize-o apenas em manipuladores de eventos.
(2) ⚠️ Evite usar .Result e .Wait()
Chamar .Result ou .Wait() em um contexto de interface de usuário (UI) ou ASP.NET pode causar um impasse: o método assíncrono aguarda que o chamador libere a thread, enquanto o chamador aguarda que o método assíncrono seja concluído — ambos os lados ficam esperando um pelo outro.
(3) ⚠️ ConfigureAwait(false)
O código da biblioteca deve utilizar ConfigureAwait(false) para evitar a captura do contexto de sincronização, prevenindo impasses e melhorando o desempenho.
▶ Exemplo
using System;
using System.Threading.Tasks;
async Task```<string>``` BadMethodAsync()
{
await Task.Delay(100);
return "Done";
}
async Task SafeUsageAsync()
{
string result = await BadMethodAsync();
Console.WriteLine("Correct usage: " + result);
}
async Task LibraryMethodAsync()
{
string result = await BadMethodAsync().ConfigureAwait(false);
Console.WriteLine("Library usage: " + result);
}
await SafeUsageAsync();
await LibraryMethodAsync();
Correct usage: Done
Library usage: Done
❓ Perguntas Frequentes
P: Qual é a diferença entre
async voideasync Task? R:async voidnão pode ser aguardado e as exceções não podem ser capturadas; deve ser usado apenas para manipuladores de eventos.async Taské a abordagem padrão — pode ser aguardado e as exceções podem ser tratadas.
P: Quando devo usar o Task.Run? R: Apenas para tarefas computacionais que exigem muito da CPU. As operações de E/S devem usar APIs assíncronas nativas (como ReadAsync), sem serem envolvidas pelo Task.Run.
P: Que problemas o .Result pode causar? R: Em contextos de sincronização UI/ASP.NET, pode ocorrer um impasse — a thread chamadora aguarda a conclusão da Task, enquanto a Task aguarda a liberação do contexto de sincronização, causando um bloqueio mútuo.
P: Quando devo usar ConfigureAwait(false)? R: Use-o no código de bibliotecas para evitar a captura do contexto de sincronização, prevenindo impasses e melhorando o desempenho. O código da camada de aplicação da interface do usuário (UI) e do ASP.NET geralmente não precisa disso.
📖 Resumo
- O código síncrono bloqueia as threads; o código assíncrono libera as threads enquanto aguarda, melhorando a capacidade de resposta e a taxa de processamento
Taskrepresenta uma operação assíncrona sem valor de retorno;Task<T>representa uma operação assíncrona com valor de retornoasyncindica um método assíncrono;awaitaguarda uma operação assíncrona sem bloquearTask.Runtransfere tarefas que exigem muito da CPU para o conjunto de threadsTask.WhenAllaguarda a conclusão de todos;Task.WhenAnyaguarda a conclusão do primeiroTask.Delayé a versão assíncrona doThread.Sleep- Os métodos assíncronos devem usar o sufixo
Async CancellationTokenimplementa o cancelamento cooperativo, controlado por meio deCancellationTokenSourceIAsyncEnumerable<T>+await foreachhabilita fluxos assíncronos- Evite
async void,.Result,.Wait(); useConfigureAwait(false)no código da biblioteca
📝 Exercícios
- Escreva um método assíncrono
DownloadAllAsyncque utilizeTask.WhenAllpara baixar três fontes de dados simuladas simultaneamente e retorne uma matriz com todos os resultados - Escreva um método de contagem regressiva assíncrona que suporte
CancellationToken, exiba os segundos restantes a cada segundo e imprima “Contagem regressiva cancelada” quando for cancelada - Use
IAsyncEnumerable<int>para gerar os primeiros 20 números de Fibonacci, repita o processo comawait foreache exiba cada um deles - Identifique o problema no código a seguir e corrija-o:
async void Button_Click() { var data = GetDataAsync().Result; Console.WriteLine(data); }