DEV Community

loading...

Trabalhando com Tasks assíncronas em C#

marcosbelorio profile image Marcos Belorio ・5 min read

Neste artigo vamos abordar os conceitos básicos e a utilização de tarefas assíncronas no C#, mostrando duas formas distintas de se trabalhar e quais são suas diferenças.

Vamos começar criando um trecho de código que execute de forma síncrona, e entender como ele é executado.

[HttpGet("TesteSincrono")]
public IActionResult TesteSincrono()
{
    Console.WriteLine("Iniciando teste síncrono");

    var resultadoApi1 = ApiConsulta1();
    var resultadoApi2 = ApiConsulta2();
    var resultadoApi3 = ApiConsulta3();

    Console.WriteLine("Finalizando teste síncrono");
    return Ok();
}

private string ApiConsulta1()
{
    Console.WriteLine("Iniciando ApiConsulta1");

    //simulando uma chamada de 1 segundo
    Console.WriteLine("Executando chamada ApiConsulta1");
    Thread.Sleep(1000);

    Console.WriteLine("Finalizando ApiConsulta1");
    return "1"; //simulando um retorno
}

private string ApiConsulta2()
{
    Console.WriteLine("Iniciando ApiConsulta2");

    //simulando uma chamada de 2 segundos
    Console.WriteLine("Executando chamada ApiConsulta2");
    Thread.Sleep(2000);

    Console.WriteLine("Finalizando ApiConsulta2");
    return "2"; //simulando um retorno
}

private string ApiConsulta3()
{
    Console.WriteLine("Iniciando ApiConsulta3");

    //simulando uma chamada de 3 segundos
    Console.WriteLine("Executando chamada ApiConsulta3");
    Thread.Sleep(3000);

    Console.WriteLine("Finalizando ApiConsulta3");
    return "3"; //simulando um retorno
}
Enter fullscreen mode Exit fullscreen mode

Então no exemplo acima criamos um endpoint onde simulamos consumir três apis distintas e colocamos para cada uma dessas apis um tempo de espera de 1 segundo à 3 segundos que seria o tempo que a api levaria para nos responder, tudo isso para ficar evidente os resultados dos nossos testes.

Executando o código acima podemos ver que o endpoint levou 6.03 segundos para ser executado e o output do console ficou assim:
teste01

Note que foi executado um método por vez, esperando sua execução ser finalizada para que o próximo método fosse iniciado. Essa forma de executar não está errada, porém deixamos a aplicação ociosa por algum tempo aguardando o retorno dos métodos e a thread principal da aplicação "travada".

Executando tarefas assíncronas

Agora vamos utilizar o mesmo cenário de exemplo porém vamos executar os métodos que simulam uma chamada de api em uma task, sendo assim teremos o seguinte código:

[HttpGet("TesteAssincrono")]
public IActionResult TesteAssincrono()
{
    Console.WriteLine("Iniciando teste assíncrono");

    var apiConsulta1Task = Task.Run(() => ApiConsulta1());
    var apiConsulta2Task = Task.Run(() => ApiConsulta2());
    var apiConsulta3Task = Task.Run(() => ApiConsulta3());

    var resultadoApi1 = apiConsulta1Task.Result;
    var resultadoApi2 = apiConsulta2Task.Result;
    var resultadoApi3 = apiConsulta3Task.Result;

    Console.WriteLine("Finalizando teste assíncrono");
    return Ok();
}
Enter fullscreen mode Exit fullscreen mode

Note no trecho de código acima que colocamos as chamadas dos métodos de api dentro de uma Task.Run(), isso faz com que estes métodos sejam chamados em paralelo através de threads separadas da thread principal da aplicação. Tasks são executadas de forma assíncrona, sendo assim neste exemplo a thread principal da aplicação continuará executando os demais comandos enquanto as Tasks são executadas em threads separadas em paralelo. No momento em que executamos Task.Result colocamos a thread principal da aplicação esperando pelo resultado da Task, para que possamos manipular o retorno dela.

Executando o código acima podemos ver que o endpoint levou 3.03 segundos para ser executado e o output do console ficou assim:
teste02

Podemos observar que a aplicação não aguardou o primeiro método ser finalizado e já iniciou os demais, fazendo com que a aplicação demore menos tempo para executar todos os comandos, outro ponto interessante que por se tratar de paralelismo, a execução do método ApiConsulta3 ocorreu antes da execução do método ApiConsulta2, sendo que no código chamamos a ApiConsulta2 antes.

Executando tarefas assíncronas com async/await

Outra forma de trabalharmos de forma assíncrona é utilizando as palavras chaves async e await em métodos que retornam uma Task, vamos aplicar no mesmo cenário de exemplo:

[HttpGet("TesteAssincronoAsyncAwait")]
public async Task<IActionResult> TesteAssincronoAsyncAwait()
{
    Console.WriteLine("Iniciando teste assíncrono com async/await");

    var apiConsulta1Task = ApiConsulta1Async();
    var apiConsulta2Task = ApiConsulta2Async();
    var apiConsulta3Task = ApiConsulta3Async();

    var resultadoApi1 = await apiConsulta1Task;
    var resultadoApi2 = await apiConsulta2Task;
    var resultadoApi3 = await apiConsulta3Task;

    Console.WriteLine("Finalizando teste assíncrono com async/await");
    return Ok();
}

private async Task<string> ApiConsulta1Async()
{
    Console.WriteLine("Iniciando ApiConsulta1Async");

    //simulando uma chamada de 1 segundo
    Console.WriteLine("Executando chamada ApiConsulta1Async");
    await Task.Delay(1000);

    Console.WriteLine("Finalizando ApiConsulta1Async");
    return "1"; //simulando um retorno
}

private async Task<string> ApiConsulta2Async()
{
    Console.WriteLine("Iniciando ApiConsulta2Async");

    //simulando uma chamada de 2 segundos
    Console.WriteLine("Executando chamada ApiConsulta2Async");
    await Task.Delay(2000);

    Console.WriteLine("Finalizando ApiConsulta2Async");
    return "2"; //simulando um retorno
}

private async Task<string> ApiConsulta3Async()
{
    Console.WriteLine("Iniciando ApiConsulta3Async");

    //simulando uma chamada de 3 segundos
    Console.WriteLine("Executando chamada ApiConsulta3Async");
    await Task.Delay(3000);

    Console.WriteLine("Finalizando ApiConsulta3Async");
    return "3"; //simulando um retorno
}
Enter fullscreen mode Exit fullscreen mode

Como podemos ver, agora nossos métodos que simulam uma consulta de api retornam Tasks e para esperarmos o retorno da execução delas colocamos a palavra chave await na frente, sendo assim todo método que utilizar a palavra chave await dentro dele precisa ser declarado com a palavra chave async, como no nosso exemplo:
public async Task<IActionResult> TesteAssincronoAsyncAwait()

Executando o código acima podemos ver que o endpoint levou 3.03 segundos também para ser executado e o output do console ficou assim:
teste03

Este exemplo da forma que foi escrito também levou menos tempo para ser totalmente executado, então podemos dizer que utilizando as palavras chaves async e await executamos Tasks em threads separadas em paralelo da thread principal?

A resposta é não, essa forma de trabalhar apesar de também ser assíncrona não utiliza paralelismo, o que ocorre é que ao executar um método async Task a thread principal continua disponível para prosseguir executando outros comandos enquanto não precisarmos do retorno do método. Diferente do teste que fizemos de forma síncrona que apesar de também só utilizar a thread principal da aplicação, no teste síncrono a thread principal ficou aguardando o retorno do método para prosseguir executando os demais comandos.

Agora uma última curiosidade, executando o exemplo acima dessa forma aqui:

[HttpGet("TesteAssincronoAsyncAwait")]
public async Task<IActionResult> TesteAssincronoAsyncAwait()
{
    Console.WriteLine("Iniciando teste assíncrono com async/await");

    var resultadoApi1 = await ApiConsulta1Async();
    var resultadoApi2 = await ApiConsulta2Async();
    var resultadoApi3 = await ApiConsulta3Async();

    Console.WriteLine("Finalizando teste assíncrono com async/await");
    return Ok();
}
Enter fullscreen mode Exit fullscreen mode

O código levará 6 segundos novamente para ser executado, isso porque a cada execução aos métodos que simulam uma consulta de api já colocamos a thread principal para aguardar o retorno com o await, então mesmo a Task sendo executada de forma assíncrona, não ganhamos otimização de tempo de execução, porém ainda assim é uma abordagem melhor do que trabalhar de forma síncrona porque deixamos a nossa thread principal disponível.

Espero que este artigo possa ajudar a esclarecer melhor a utilização de Tasks em C#, sintam-se a vontade para debater sobre o assunto aqui.

Referência:
Programação assíncrona com async e await

Discussion (1)

pic
Editor guide
Collapse
pedrodantas profile image
Pedro Dantas

Valeu! Muito esclarecedor, não é bicho de 7 cabeças quanto parece ser