A programação assíncrona é fundamental para desenvolver aplicações responsivas e eficientes em C#. O modificador async desempenha um papel central nesse paradigma, permitindo que métodos sejam definidos como assíncronos e, consequentemente, utilizem o operador await para aguardar a conclusão de operações sem bloquear a thread de execução principal.
Conceitos Básicos de Tarefas (Task)
Antes de mergulhar no async, é útil entender como as operações assíncronas são encapsuladas em C# usando objetos Task. Uma Task representa uma operação que pode ser executada em segundo plano e retornar um resultado no futuro.
O exemplo a seguir ilustra diferentes formas de criar e iniciar objetos Task, bem como aguardar sua conclusão:
using System;
using System.Threading;
using System.Threading.Tasks;
public class DemonstraçãoDeTarefas
{
public static void Main()
{
// Define uma ação genérica que será executada pelas tarefas.
// Ela exibe o ID da tarefa, o dado recebido e o ID da thread atual.
Action<object> acaoDaTarefa = (object dado) =>
{
Console.WriteLine($"Tarefa ID: {Task.CurrentId}, Dado: {dado}, Thread: {Thread.CurrentThread.ManagedThreadId}");
};
Console.WriteLine($"Thread Principal ID: {Thread.CurrentThread.ManagedThreadId}");
// 1. Criar uma tarefa sem iniciá-la imediatamente.
Task tarefaNaoIniciada = new Task(acaoDaTarefa, "primeiro_dado");
Console.WriteLine("Tarefa 'primeiro_dado' criada, mas não iniciada.");
// 2. Criar e iniciar uma tarefa usando Task.Factory.StartNew.
// Este método era comum antes de Task.Run.
Task tarefaViaFactory = Task.Factory.StartNew(acaoDaTarefa, "segundo_dado");
Console.WriteLine("Tarefa 'segundo_dado' iniciada via Factory.StartNew. Aguardando...");
tarefaViaFactory.Wait(); // Bloqueia a thread principal até a conclusão desta tarefa.
// 3. Iniciar a tarefa que foi criada anteriormente.
tarefaNaoIniciada.Start();
Console.WriteLine("Tarefa 'primeiro_dado' foi iniciada. Aguardando...");
tarefaNaoIniciada.Wait(); // Aguarda a conclusão da tarefa 'primeiro_dado'.
// 4. Criar e iniciar uma tarefa usando Task.Run.
// Este é o método preferido para iniciar tarefas em segundo plano.
string dadoParaRun = "terceiro_dado";
Task tarefaViaRun = Task.Run(() =>
{
Console.WriteLine($"Tarefa ID: {Task.CurrentId}, Dado: {dadoParaRun}, Thread: {Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine("Tarefa 'terceiro_dado' iniciada via Task.Run. Aguardando...");
tarefaViaRun.Wait(); // Aguarda a conclusão da tarefa 'terceiro_dado'.
// 5. Criar uma tarefa e executá-la sincronicamente na thread atual.
// Task.RunSynchronously é menos comum e executa a tarefa imediatamente na thread chamadora.
Task tarefaSincrona = new Task(acaoDaTarefa, "quarto_dado");
Console.WriteLine("Executando tarefa 'quarto_dado' sincronicamente na thread principal...");
tarefaSincrona.RunSynchronously();
tarefaSincrona.Wait(); // Embora síncrona, é boa prática aguardar para capturar exceções.
Console.WriteLine("Todas as demonstrações de tarefas foram concluídas.");
}
}
A execução do código acima pode produzir uma saída similar a esta, com IDs de thread variando conforme o escalonador de tarefas:
Thread Principal ID: 1
Tarefa 'primeiro_dado' criada, mas não iniciada.
Tarefa 'segundo_dado' iniciada via Factory.StartNew. Aguardando...
Tarefa ID: 2, Dado: segundo_dado, Thread: 3
Tarefa 'primeiro_dado' foi iniciada. Aguardando...
Tarefa ID: 3, Dado: primeiro_dado, Thread: 4
Tarefa 'terceiro_dado' iniciada via Task.Run. Aguardando...
Tarefa ID: 4, Dado: terceiro_dado, Thread: 5
Executando tarefa 'quarto_dado' sincronicamente na thread principal...
Tarefa ID: 5, Dado: quarto_dado, Thread: 1
Todas as demonstrações de tarefas foram concluídas.
Entendendo o Modificador async
O modificador async é usado para marcar um método, uma expressão lambda ou um método anônimo como assíncrono. Isso sinaliza ao compilador que o método contém um ou mais operadores await e pode ser pausado e retomado sem bloquear a thread chamadora.
Por exemplo, um método assíncrono pode ser declarado da seguinte forma:
public async Task<int> CalcularAsync()
{
// ... lógica assíncrona ...
return 42;
}
Quando um método assíncrono encontra seu primeiro operador await, ele pausa sua execução e retorna o controle para o chamador. A thread que invocou o método fica livre para executar outras tarefas. A execução do método assíncrono é retomada quando a operação aguardada é concluída. Isso é crucial para manter a responsividade em interfaces de usuário ou para liberar recursos de servidor em aplicações web.
É importante notar que um método marcado com async, mas que não contém nenhum operador await, será executado de forma síncrona. O compildaor emitirá um aviso nesse caso, pois a ausência de await geralmente indica um erro ou uma intenção não realizada de assincronicidade.
O termo async é uma palavra-chave contextual. Isso significa que ele é interpretado como uma palavra-chave apenas quando usado como um modificador de método, lambda ou método anônimo. Em outros contextos, ele pode ser usado como um identificador comum.
Exemplo Prático: Uma Aplicação WPF com async e await
Este exemplo demonstra o fluxo de controle em uma aplicação de interface gráfica (WPF), onde um evento de clique de botão assíncrono interage com um método assíncrono para buscar dados da web sem congelar a UI. Para reproduzir, você precisaria de um projeto WPF com um Button chamado BotaoIniciar e um TextBox ou TextBlock chamado CaixaTextoResultados no seu XAML.
No seu arquivo XAML (por exemplo, MainWindow.xaml), adicione elementos como:
<Button Content="Iniciar Processo" Name="BotaoIniciar" Click="BotaoIniciar_Click"/>
<TextBox Name="CaixaTextoResultados" AcceptsReturn="True" IsReadOnly="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"/>
No seu arquivo code-behind (por exemplo, MainWindow.xaml.cs), adicione o seguinte código. Certifique-se de incluir os using necessários, como System.Net.Http e System.Windows.
using System;
using System.Net.Http;
using System.Threading.Tasks; // Necessário para Task e Task<TResult>
using System.Windows; // Para RoutedEventArgs e UI elements
// ... dentro da classe MainWindow (ou similar) ...
private async void BotaoIniciar_Click(object sender, RoutedEventArgs e)
{
CaixaTextoResultados.Text = "Iniciando operação assíncrona...\n"; // Limpa e informa o início
try
{
// Chama o método assíncrono e aguarda seu resultado.
// A thread da UI não é bloqueada. O controle retorna aqui quando ObterTamanhoPaginaAsync completa.
int tamanhoConteudo = await ObterTamanhoPaginaAsync();
CaixaTextoResultados.Text += $"Processo concluído. Tamanho da página: {tamanhoConteudo:N0} caracteres.\n";
}
catch (HttpRequestException httpEx)
{
// Captura exceções específicas de requisições HTTP (ex: falha de conexão, 404)
CaixaTextoResultados.Text += $"Erro de HTTP: {httpEx.Message}\n";
}
catch (Exception ex)
{
// Captura quaisquer outras exceções gerais
CaixaTextoResultados.Text += $"Ocorreu um erro inesperado: {ex.Message}\n";
}
}
/// <summary>
/// Realiza uma requisição HTTP assíncrona para obter o conteúdo de uma página web
/// e retorna o número de caracteres do conteúdo.
/// </summary>
/// <returns>O número de caracteres da página web.</returns>
public async Task<int> ObterTamanhoPaginaAsync()
{
// Recomenda-se usar uma única instância de HttpClient para toda a vida útil da aplicação,
// mas para fins de demonstração simples, criamos uma nova instância.
using var clienteWeb = new HttpClient();
string urlParaBuscar = "https://www.google.com"; // Alvo da requisição
CaixaTextoResultados.Text += $"Buscando conteúdo de: {urlParaBuscar}...\n";
// O método 'GetStringAsync' retorna um Task<string>.
// 'await' pausa este método até que a string seja completamente baixada.
// A thread da UI continua responsiva.
string conteudoDaPagina = await clienteWeb.GetStringAsync(urlParaBuscar);
// Esta linha só será executada após 'GetStringAsync' ter concluído e retornado seu resultado.
CaixaTextoResultados.Text += "Conteúdo da página obtido com sucesso. Calculando...\n";
return conteudoDaPagina.Length;
}
A saída no CaixaTextoResultados será semelhante a:
Iniciando operação assíncrona...
Buscando conteúdo de: https://www.google.com...
Conteúdo da página obtido com sucesso. Calculando...
Processo concluído. Tamanho da página: 12345 caracteres.
Tipos de Retorno de Métodos Assíncronos
Um método assíncrono pode ter os seguintes tipos de retorno:
Task: Usado quando o método não retorna um valor significativo. ATaskrepresenta a conclusão da operação.Task<TResult>: Usado quando o método retorna um valor do tipoTResultapós sua conclusão.void: Geralmente, é desaconselhado usarasync void, exceto para manipuladores de eventos (comoBotaoIniciar_Clickno exemplo). Métodosasync voidnão podem ser aguardados (await) por seus chamadores, e é mais difícil para eles propagarem exceções.- Tipos com
GetAwaiter: A partir do C# 7.0, qualquer tipo que implemente um métodoGetAwaiteracessível pode ser um tipo de retorno. Um exemplo notável éSystem.Threading.Tasks.ValueTask<TResult>, que pode reduzir alocações de memória em cenários de alta performence ao retornar um valor em vez de uma referência a um objetoTask. Para usarValueTask<TResult>, você pode precisar adicionar o pacote NuGetSystem.Threading.Tasks.Extensions.
Métodos assíncronos não podem declarar parâmetros in, ref ou out, nem ter um valor de retorno por referência. No entanto, eles podem invocar outros métodos que utilizam esses tipos de parâmetros.