Programação assíncrona em C#

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


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.

Neste 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. Enquanto isso, 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 conteudo.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:

Chamando um método assíncrono

Em relação a um método síncrono, geralmente não precisamos de grandes alterações. Na prática, o método não retornará diretamente o valor final, mas um objeto Task que representa uma operação em andamento. Com esta Task podemos esperar que a operação termine ou cancelá-la a qualquer momento.

Dentro de um método síncrono:

void MetodoSincrono()
{
  var tarefa = MetodoAssincrono();
  // Continua o processamento enquanto a tarefa não termina
  var x = tarefa.Result;
}

async Task MetodoAssincrono()
{
  // ...
}

Com uma tarefa que retorna valor, Result espera pelo término do processamento para então devolver o valor de retorno do método.

Dentro de um método que também é assíncrono podemos fazer a chamada assíncrona e usar o operador await para esperar pelo resultado num único comando:

async Task MetodoAssincrono()
{
  var x = await OutroMetodoAssincrono();
  …
}

Esta forma pode ser usada quando não precisamos fazer mais nada entre a chamada do método e o seu término. Ainda assim a execução será suspensa até que o resultado esteja disponível.

Com uma tarefa que não retorna valor, podemos esperar o término com o método Wait:

void MetodoSincrono()
{
    var tarefa = MetodoAssincrono();
    // O processamento continua enquanto a tarefa não termina
 
    // Espera pelo término da tarefa
    tarefa.Wait();
}
 
async Task MetodoAssincrono()
{
    ...
}

Importante: dentro da thread da interface com o usuário ou de uma requisição HTTP no ASP.NET não se deve usar o método Wait para esperar uma tarefa terminar, pois ele bloqueia a thread. Deixe o framework fazer isto por você.

Cancelando tarefas em execução

Para que um método assíncrono possa ser interrompido, ele deve receber um parâmetro do tipo CancellationToken que será usado para sinalizar o cancelamento da tarefa quando for necessário:

void MetodoSincrono()
{
    var tokenSource = new CancellationTokenSource();
    var tarefa = MetodoAssincrono(tokenSource.Token);
    // cancela a tarefa imediatamente
    tokenSource.Cancel();
    // cancela a tarefa após um intervalo de tempo
    tokenSource.CancelAfter(millisecondsDelay: 1000);
}
 
async Task MetodoAssincrono(CancellationToken token)
{
    ...
    // durante a execução da tarefa
    if (token != null && token.IsCancellationRequested)
    {
        return;
   }
}

É claro que o método ou as tarefas que ele cria devem fazer esta verificação em algum momento para que a token tenha efeito. Os método assíncronos da API do .NET já fazem isto.

Sincronizando múltiplas tarefas

Há casos em que precisamos disparar várias tarefas simultaneamente. O .NET oferece mecanismos simples para sincronizá-las sem muito esforço. Veja o exemplo:

private async Task CreateMultipleTasksAsync()
{
    // Declare an HttpClient object, and increase the buffer size. The 
    // default buffer size is 65,536.
    HttpClient client =
        new HttpClient() { MaxResponseContentBufferSize = 1000000 };
 
    // Create and start the tasks. As each task finishes, DisplayResults  
    // displays its length.
    Task download1 = 
        ProcessURLAsync("http://msdn.microsoft.com", client);
    Task download2 = 
        ProcessURLAsync("http://msdn.microsoft.com/en-us/library/hh156528(VS.110).aspx", client);
    Task download3 = 
        ProcessURLAsync("http://msdn.microsoft.com/en-us/library/67w7t67f.aspx", client);
 
    // Await each task. 
    int length1 = await download1;
    int length2 = await download2;
    int length3 = await download3;
 
    int total = length1 + length2 + length3;
 
    // Display the total count for the downloaded websites.
    resultsTextBox.Text +=
        string.Format("\r\n\r\nTotal bytes returned:  {0}\r\n", total);
}

Para uma quantidade qualquer de tarefas, podemos usar dois métodos da classe Task. Dada uma lista de tarefas:

Task.WhenAll retorna uma tarefa que será completada quando todas as tarefas da lista terminarem.

Task.WhenAny retorna uma tarefa que será completada quando qualquer uma das tarefas da lista terminar.

Exemplo adaptado no MSDN (códigos completos disponíveis aqui e aqui):

List urls = new List
{
    "http://msdn.microsoft.com",
    "http://msdn.microsoft.com/en-us/library/hh290136.aspx",
    "http://msdn.microsoft.com/en-us/library/ee256749.aspx",
    "http://msdn.microsoft.com/en-us/library/hh290138.aspx",
    "http://msdn.microsoft.com/en-us/library/hh290140.aspx",
    "http://msdn.microsoft.com/en-us/library/dd470362.aspx",
    "http://msdn.microsoft.com/en-us/library/aa578028.aspx",
    "http://msdn.microsoft.com/en-us/library/ms404677.aspx",
    "http://msdn.microsoft.com/en-us/library/ff730837.aspx"
};
 
IEnumerable downloadTasksQuery = 
    from url in urls select ProcessURLAsync(url);
 
// Use ToArray to execute the query and start the download tasks.
Task[] downloadTasks = downloadTasksQuery.ToArray();
 
// You can do other work here before awaiting.
 
// Await the completion of all the running tasks. 
// int[] lengths = await Task.WhenAll(downloadTasks);
 
// Await the completion of any of the running tasks. 
int[] lengths = await Task.WhenAny(downloadTasks);

Tratamento de erros

A facilidade para escrever métodos assíncronos os torna fáceis também de depurar e o tratamento de erros geralmente não é diferente do comum, com try-catch, mas há dois casos especiais:

1. Operação cancelada: usar o await com uma tarefa cancelada causa uma exceção OperationCanceledException.

2. Um método assíncrono que retorna void não pode ser usado com await. A recomendação é que o método que o chamou possa continuar independentemente dos erros gerados durante a execução da tarefa assíncrona. Mas, se ocorre alguma exceção em qualquer método assíncrono, ela é armazenada na tarefa retornada:

public async Task DoSomethingAsync()
{
  Task theTask = DelayAsync();
 
  try
  {
    string result = await theTask;
    Debug.WriteLine("Result: " + result);
  }
  catch (Exception ex)
  {
    Debug.WriteLine("Exception Message: " + ex.Message);
  }
  Debug.WriteLine("Task IsCanceled: " + theTask.IsCanceled);
  Debug.WriteLine("Task IsFaulted:  " + theTask.IsFaulted);
  if (theTask.Exception != null)
  {
    Debug.WriteLine("Task Exception Message: "
      + theTask.Exception.Message);
    Debug.WriteLine("Task Inner Exception Message: "
      + theTask.Exception.InnerException.Message);
  }
}
 
private async Task DelayAsync()
{
    await Task.Delay(100);
 
    // Uncomment each of the following lines to 
    // demonstrate exception handling. 
 
    //throw new OperationCanceledException("canceled");
    //throw new Exception("Something happened.");
    return "Done";
}

Mais detalhes na referência do C#.

Por hoje é só. Com estes conhecimentos já é possível tirar proveito dos recursos para programação assíncrona do C#.

Deixe seus comentários abaixo.

Asynchronous programming

Anders Hejlsberg: Introducing Async – Simplifying Asynchronous Programming

Future directions for C# and Visual Basic

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.