Trabalhando com Tasks assíncronas em C#
Tags: csharp, dotnet, netcore, async
Published: 2021-01-28 13:27:18

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.

````csharp
[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
}
```
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](https://dev-to-uploads.s3.amazonaws.com/i/bl73kwupdw1sb21cc6rb.PNG)

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:

```csharp
[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();
}
```

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](https://dev-to-uploads.s3.amazonaws.com/i/1oobh1yy1jc36lknaofv.PNG)

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:

```csharp
[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
}
```
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](https://dev-to-uploads.s3.amazonaws.com/i/y8bhplyurin88mn42ua6.PNG)

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:
```csharp
[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();
}
```
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](https://docs.microsoft.com/pt-br/dotnet/csharp/programming-guide/concepts/async/)