Programação assíncrona em C#

Este artigo foi publicado originalmente no blog da Tecsystem, agora revisado e atualizado.

Atenção: este artigo está sendo incrementando à medida que o conteúdo é revisado e atualizado. Guarde este endereço e volte em breve para ver as atualizações.


Num mundo de dispositivos e aplicativos conectados, a programação assíncrona não é mais uma opção que um bom desenvolvedor pode simplesmente ignorar.

Para criar uma boa interação com o usuário, qualquer software, em qualquer plataforma, precisa executar tarefas em segundo plano de forma que a interface não deixe de responder aos comandos.

Tópicos deste post:

Por que programação assíncrona?

O usuário pode querer cancelar a visualização de uma foto antes que ela tenha sido carregada por completo, enviar uma nova mensagem enquanto a anterior ainda não foi processada ou simplesmente abrir uma página da Web sem que isto o impeça de continuar interagindo com o aplicativo.

Imagine fazer qualquer uma destas tarefas e ser obrigado a esperar enquanto ela não terminar. Por isso a programação assíncrona é necessária em boa parte das funcionalidades que existem em softwares comuns, que acessam a internet, leem arquivos, fazem pesquisas e outras tarefas rotineiras e, às vezes, demoradas.

“Asynchronous programming is becoming the norm in modern, connected applications”

Anders Hejlsberg

A programação assíncrona está se tornando a norma em aplicativos modernos e conectados.

Para melhorar o desempenho e a responsividade das aplicações, podemos e devemos usar operações assíncronas.

Tradicionalmente, este tipo de operação costumava ser mais difícil de implementar, testar e depurar, mas a programação assíncrona em C# ficou bem mais fácil com recursos lançados desde a versão 5.0 da linguagem.

Síncrono x assíncrono

Pense numa coisa que (quase) todo mundo gosta: café! Agora, suponha que você vai ligar a cafeteira para fazer um café fresco.

É claro que o café tem que ficar pronto para você poder bebê-lo. Mas enquanto não fica, você pode responder uma mensagem, ir ao banheiro ou fazer qualquer outra coisa.

E se você tivesse que ficar lá olhando para a cafeteira enquanto o café não sai?

Da mesma maneira, na chamada de um método síncrono, não há como continuar o processamento enquanto o método não terminar.

Durante este tempo, o programa para de responder, não pode atualizar a interface com o usuário e não recebe nenhum comando, a não ser que você use algum mecanismo para isso.

É comum o usuário dizer que o programa “travou”, por exemplo, durante uma consulta ao banco de dados.

Por outro lado, um método assíncrono pode retornar o fluxo de execução para quem o chamou mesmo que a operação ainda não tenha sido completada.

Enquanto isso é possível informar ao usuário que uma operação está em andamento ou até interromper o processamento antes que ela termine.

O ponto chave é que o programa pode continuar sem ter que esperar operações demoradas terminarem. Por exemplo, baixar dados da internet.

int AcessarWeb()
{
  var cliente = new WebClient();
  var conteudo = cliente.DownloadString("https://google.com");
  return conteudo.Length;
}

Implementando desse jeito, temos que esperar o download terminar. Até lá, paciência.

Mas não precisamos parar a execução do programa até que a operação termine. Melhor que isso, podemos ser notificados quando ela for concluída, com ou sem sucesso.

Programação assíncrona com .NET

Normalmente, para fazer processamento assíncrono no .NET, uma das maneiras era usar callbacks. Quando a tarefa termina, um método (callback) é chamado para tratar o resultado.

Este método pode ser enxergado com uma continuação da ação que foi iniciada. Quando ela terminar, execute o callback. Isto é típico de uma implementação baseada em eventos.

Nesse exemplo, definimos um evento para informar quando o download terminar:

void AcessarWebComCallback()
{
  var cliente = new WebClient();
  cliente.DownloadStringCompleted += cliente_DownloadStringCompleted;
  cliente.DownloadStringAsync(new Uri("https://google.com"));
}
 
void cliente_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
  Console.WriteLine(e.Result.Length);
}

Repare o uso do método DownloadStringAsync, que é um pouco melhor mas ainda não é o ideal.

Escrevendo um método assíncrono com async e await

Agora vamos reescrever o código sem usar um callback:

async Task<int> AccessarWebAsync()
{
  var client = new WebClient();
  string conteudo = await 
    client.DownloadStringTaskAsync("http://google.com");
  return urlContents.Length;
}

A principal diferença fica por conta das palavras async e await, que são facilidades que a linguagem oferece para que o desenvolvedor não tenha que se preocupar com os detalhes da manipulação das tarefas assíncronas.

O que acontece por trás do código é que a cada vez que se usa o await, o restante do método é registrado como a continuação da tarefa. Então o método continua a partir do comando seguinte assim que essa tarefa termina.

Outro exemplo criando uma tarefa explicitamente no corpo do método:

async Task SleepAsync()
{
  var tarefa = new Task(() => Thread.Sleep(5000));
  tarefa.Start();
  await tarefa;
}

Nesse caso, criamos uma tarefa que vai ficar parada por 5 segundos e depois terminar. Pode não fazer muito sentido mas, para fins didáticos, exemplifica bem a questão: quem chamar esse método não vai ficar esperando esse tempo passar.

Entendendo os conceitos

Aqui vão dois conceitos importantes:

  • async é um modificador, assim como private, public ou override, por exemplo. Ele sinaliza que o método é assíncrono e pode ser chamado como tal;
  • await é um operador que faz com que a execução do método seja suspensa caso a tarefa que o acompanha não tenha sido completada. O código após o await só será executado quando a tarefa terminar.

Em resumo, um método assíncrono:

  • É marcado com o modificador async;
  • Deve retornar algo do tipo Task ou Task<TResult>, onde TResult é o tipo do resultado da operação assíncrona. Um método que não retorna nada ou contém um return apenas para controlar o fluxo de execução, deve retornar uma Task;
  • Usa o operador await para devolver o controle da execução para quem o chamou. A ausência do await no corpo do método gera uma advertência do compilador informando que o método será executado sincronamente, mesmo sendo marcado com async;
  • Inicia uma ou mais operações que podem ser um outro método assíncrono, uma Task ou Task<TResult> criada explicitamente ou qualquer coisa que implemente o padrão awaiter;
  • Continua sua execução quando a tarefa em espera termina;
  • Não necessita de outra thread para ser executado. O método sempre executa no contexto de sincronização corrente e ocupa tempo da thread somente quando está ativo;
  • Por convenção, tem seu nome seguido do sufixo Async.

Além disto, este “syntactic sugar” torna o código bem mais simples porque abstrai os detalhes do gerenciamento destas tarefas.

Para executar tarefas que exigem muito processamento em segundo plano, é recomendável chamar Task.Run, que combina o poder do processamento multithread com a facilidade do modelo de programação usando async e await.

A API do .NET traz diversos métodos assíncronos. Só para citar alguns exemplos:

▶ Na próxima parte: o que você precisa saber para chamar um método assíncrono.

Foto de Lina Kivaka no Pexels

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.