C# - Usando async/await e Task.WhenAll para otimizar o código
Hoje veremos como usar async/await e Task.WhenAll para melhorar a velocidade de execução do código. |
Na linguagem C# As palavras-chave async e await são usadas para programação assíncrona.
Usamos o modificador async na declaração de um método para indicar que o método é assíncrono e neste caso ele depende da palavra-chave await.
Isso ocorre porque, ao usar o await, seu programa deve "esperar" um resultado, e essa espera é feita em segundo plano de forma assíncrona; assim, o await espera até que o método retorne o seu resultado, e isso ocorre sem bloquear o fluxo do programa.
Dessa forma a palavra-chave await deve ser usada para receber o resultado de uma Task ou Tarefa.
Assim um await recebe um argumento - awaitable - que é uma operação assíncrona e existem dois tipos disponíveis de awaitables na plataforma .NET Task<T> e Task.
O
Await examina o
que está pendente para ver se já foi concluído; se o awaitable já tiver
sido concluído, o método apenas continuará executando (de forma síncrona,
como um método normal).
Se “await” vê que o awaitable não foi concluído, ele age de forma assíncrona.
Ele diz ao awaitable para executar o restante do método quando ele for
concluído e, em seguida, retorna do método assíncrono.
Mais tarde, quando o awaitable for concluído, ele executará o restante do
método assíncrono. Se você estiver aguardando um tipo de espera integrado
(como uma task), o restante do método assíncrono será executado em um "contexto"
que foi capturado antes do retorno de "await".
Neste artigo veremos como usar o método Task.WhenAll() que cria uma tarefa que será concluída quando todas as tarefas fornecidas forem concluídas.
Vamos mostrar que usando este recurso podemos aumentar o desempenho do código na execução de operações assíncronas.
Criando o projeto Console
Vamos criar um projeto Console do tipo .NET Core chamado CShp_Async1 no VS 2019 Community:
Vamos criar dois métodos assíncronos que iremos usar em nosso projeto.
O primeiro será um método ConcatenaAsync() que a partir da geração de números inteiros em um intervalo, retorna uma string que é a concatenação dos caracteres ASC que representam esses números:
1- ConcatenaAsync()
private static async Task<string> ConcatenaAsync()
{
var palavra = string.Empty;
//gera numeros inteiros de 65 a 91(A a Z) e concatena
foreach (var contador in Enumerable.Range(65, 26))
{
palavra = string.Concat(palavra, (char)contador);
await Task.Delay(150);
}
return palavra;
}
|
A seguir vamos criar outro método SomaAsync() que a partir da geração de números inteiros em um intervalo, soma esses números e retorna a soma.
2- SomaAsync()
private static async Task<int> SomaAsync() { int soma = 0; //gera 25 números a partir do zero e soma foreach (var contador in Enumerable.Range(0, 25)) { soma += contador; await Task.Delay(100); } return soma;
}
|
Temos assim dois métodos assíncronos que retornam um Task<string> e Task<int>.
Vamos agora definir o código que vai usar esses métodos.
Executando o código assíncrono
No método Main da classe Program inclua o código abaixo que vai chamar os métodos SomaAsync() e ConcatenaAsync() e exibir o resultado e o tempo decorrido:
private static async Task Main(string[] args)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
var soma = await SomaAsync();
Console.WriteLine("Tempo decorrido após a soma terminar..." + stopwatch.Elapsed);
var concatena = await ConcatenaAsync();
Console.WriteLine("Tempo decorrido após soma e concatenação terminarem..." + stopwatch.Elapsed);
Console.WriteLine($"Resultado da soma = {soma}");
Console.WriteLine($"Resultado da concatenação = {concatena}");
Console.Read();
}
|
Executando o projeto temos o seguinte resultado :
Apesar da utilização da palavra-chave await e do método Main estar marcado com a palavra-chave async este código vai executar os dois métodos de forma síncrona.
Podemos perceber isso pelo resultado obtido acima onde notamos que os métodos são executados consecutivamente:
Assim o tempo total gasto para executar as duas tarefas é a soma dos tempos gastos em cada tarefa, ou seja, quase 10 s.
Agora veremos o comportamento da execução deste dois métodos usando o método Task.WhenAll().
Executando o código assíncrono com Task.WhenAll()
Agora vamos usar o método Task.WhenAll() para criar uma tarefa que será concluída se e somente se todas as outras tarefas foram concluídas.
Veja como deve ficar o código agora:
private static async Task Main(string[] args)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
var somaTask = SomaAsync();
var concatenaTask = ConcatenaAsync();
await Task.WhenAll(somaTask, concatenaTask);
Console.WriteLine($"Tempo decorrido após soma e concatenação terminarem : {stopwatch.Elapsed}");
Console.WriteLine($"Resultado da soma = {somaTask.Result}");
Console.WriteLine($"Resultado da concatenação = {concatenaTask.Result}");
Console.Read();
}
|
Agora estamos chamando cada um dos métodos sem usar a palavra await.
E definimos a criação da tarefa usando o método WhenAll() relacionando as duas tarefas que serão executadas.
Assim a tarefa criada acima somente será concluída quando os dois métodos assíncronos terminarem a sua execução.
Abaixo temos o resultado obtido:
Note que o tempo gasto é cerca de 4,15 s, ou seja, duas vezes mais rápido que o obtido na primeira execução.
Isso ocorre porque estamos executando os dois métodos em paralelo e fazendo uso total dos métodos assíncronos de oportunidade presentes.
Agora, nosso tempo total de execução é tão lento quanto o método mais lento (quase isso) , em vez de ser o tempo cumulativo para todos os métodos executando um após o outro.
Com isso espero que fique claro como o método Task.WhenAll() opera e como você pode usá-lo para otimizar o seu código nestes cenários.
Pegue o código do projeto aqui: CShp_Async1.zip
E a vida eterna é
esta: que te conheçam a ti, o único Deus verdadeiro, e a Jesus Cristo, a quem
enviaste.
João 17:3
Referências:
C# - Tasks x Threads. Qual a diferença - Macoratti.net
C# - Programação Assíncrona como : Asycn e Task
C# - O Struct Guid - Macoratti.net