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:


José Carlos Macoratti