.NET -  Considerações sobre Async e Await


 Hoje vamos fazer algumas considerações sobre a programação assíncrona na plataforma .NET.

A programação assíncrona carrega alguns mitos, dentre eles o de que usar a programação assíncrona vai tornar o seu código mais rápido. (traduzido parcialmente do artigo https://exceptionnotfound.net/async-await-in-asp-net-csharp-ultimate-guide/ )

Mas isso é um equívoco, pois a programação assíncrona não vai fazer com que o seu código seja executado com mais rapidez.

Como assim !!!!

Calma, deixa eu continuar...

O que a programação assíncrona realmente faz é aumentar a quantidade de solicitações que podem ser tratadas ao mesmo tempo com os mesmos recursos. A mesma quantidade de threads pode lidar com muito mais solicitações simultâneas em um sistema assíncrono do que em um síncrono.

Resumindo, a programação assíncrona não melhora o desempenho, melhora o rendimento.

O que ocorre é que o resultado do código assíncrono faz parecer que o código esta sendo executado mais rápido, mas na realidade ele esta apenas fazendo mais de uma vez, permitindo mais rendimento.

Vamos fazer uma analogia com um restaurante.

Em um restaurante, quando os pedidos são feitos, eles são entregues na cozinha para serem preparados. A cozinha tem um monte de cozinheiros cujo trabalho é fazer a comida necessária para cada pedido. Existem dois tipos de cozinha que podemos usar:  a síncrona e a assíncrona.

1- A cozinha síncrona

Em uma cozinha síncrona, cada cozinheiro recebe um pedido a qualquer momento.

Cada cozinheiro deve, então, trabalhar esse pedido até a conclusão por conta própria. Isso significa que se um item precisar ir ao forno, eles ficarão esperando pacientemente enquanto o item estiver cozinhando e não farão mais nada durante esse tempo.

Portanto, esta cozinha pode lidar com apenas uma quantidade X de pedidos simultaneamente, onde X é o número de cozinheiros na cozinha.

2- A cozinha assíncrona

Em uma cozinha assíncrona, os cozinheiros não terão tempo ocioso. Eles podem receber um pedido e começar a fazê-lo, mas se chegar a um ponto em que terão que esperar que algo seja feito (cozinhar no forno, etc.), eles devem ir e fazer outra coisa.

Esta cozinha pode lidar com muito mais pedidos do que cozinheiros, porque nenhum cozinheiro jamais ficará parado esperando que algo aconteça. Desta forma, o número de pedidos atendidos (também conhecido como processamento) da cozinha aumenta, embora cada cozinheiro não trabalhe mais rápido do que normalmente.

Esta cozinha é a que mais se aproxima do mundo real.

Os fundamentos da programação assíncrona

Como o objetivo da programação assíncrona na plataforma .NET é melhorar o rendimento, podemos começar a falar sobre alguns dos fundamentos que devemos ter em mente ao tentar implementá-la.

Vamos começar definindo um conceito importante :  Assíncrono não é a mesma coisa que multithreading.

É de vital importância lembrar que o código assíncrono NÃO é o mesmo que o código que usa multithreading.

Durante o processo de execução de uma tarefa assíncrona, a plataforma .NET criará algo chamado SynchronizationContext que armazena o contexto necessário para que uma thread retome a execução naquele ponto.

A classe SynchronizationContext fornece a funcionalidade básica para propagar um contexto de sincronização em vários modelos de sincronização. O objetivo do modelo de sincronização implementado por esta classe é permitir que as operações assíncronas/síncronas internas do common language runtime se comportem adequadamente com diferentes modelos de sincronização. Este modelo também simplifica alguns dos requisitos que os aplicativos gerenciados devem seguir para funcionar corretamente em diferentes ambientes de sincronização.

Observe que a thread que faz isso pode ou não ser a mesma thread que executou o código antes desse ponto.

Nota: Veja este post de Eric Lippert tratando deste assunto: Difference between asynchronous and multithreading

Em resumo temos que : "Threading é sobre os trabalhadores; a assincronismo é sobre tarefas."

Como a programação assíncrona trata de tarefas, precisamos especificar quais tipos de tarefas se beneficiam com seu uso. Essas tarefas são chamadas de E/S(entrada/saida) ou I/O (input/output) , para diferenciá-las das tarefas de CPU.

As tarefas vinculadas à CPU são tarefas que dependem da velocidade de computação da máquina para serem executadas rapidamente, como cálculos matemáticos complexos. Esses cálculos vão ocupar o tempo do processador e, enquanto estão sendo executados, o processador não precisa esperar por nenhuma outra entrada. Esses tipos de tarefas não se beneficiam da programação assíncrona.

As tarefas associadas às operações I/O ou E/S são aquelas que requerem uma resposta de fontes externas. Essas fontes externas podem incluir um banco de dados, um serviço, uma API REST ou outros. Ao fazer chamadas para essas fontes, o processador geralmente precisa "esperar" que elas respondam.

Em um ambiente "normal" (ou seja, um síncrono), a thread que executa o código irá apenas sentar e esperar que a fonte externa responda após tê-lo chamado.

A programação assíncrona permite que a thread deixe um "marcador" no ponto em que normalmente teria que esperar para que uma thread possa retornar a esse ponto em um momento posterior, quando a fonte externa terá respondido. (É este marcador que inclui o SynchronizationContext citado anteriormente.)

Desta forma a programação assíncrona não oferece nenhum benefício para tarefas vinculadas à CPU; pois não há "espera" envolvida.

Async se espalha como um vírus

Quando alguém começa a implementar a programação assíncrona no código usado na plataforma .NET, tende a notar a rapidez com que ele se propaga para o próximo código. É comparável a um vírus; gosta de infectar coisas com as quais entra em contato.

O que isso significa é que não devemos combinar código síncrono e assíncrono sem saber exatamente quais consequências isso vai acarretar.

Programação assíncrona : Tipos de Retorno

O código assíncrono na plataforma .NET usa apenas três tipos de retorno:

No entanto, o número de casos de uso válidos para retornar void é muito pequeno. Isso se deve principalmente ao fato de que, ao retornar void, o sistema não terá ideia de quando (ou mesmo se) o método será concluído.

Além disso, o tratamento de exceções fica comprometido ao retornar void. Portanto, a melhor recomendação é não retornar void de tarefas assíncronas (embora haja uma exceção notável, que são os manipuladores de eventos).

Diretriz para Refatoração

Todos os fundamentos acima são verdadeiros se você estiver escrevendo um novo aplicativo assíncrono do zero ou refatorando um aplicativo síncrono usando async e await. Mas há uma orientação adicional que devemos observar ao refatorar um aplicativo síncrono em um assíncrono:  Refatore "de baixo para cima" para menos dependências

Como a programação assíncrona se espalha como um vírus, sua melhor aposta ao refatorar para ela é começar no nível mais baixo possível de sua arquitetura de dados e continuar.

Assim a implementação de programação assíncrona na plataforma .NET usando async/await permite que um sistema trate muito mais solicitações no mesmo hardware, aumentando a taxa de transferência (e não o desempenho) desse sistema.

Fazemos isso encapsulando tarefas vinculadas a operações de  I/O ou E/S. O código assíncrono tende a se espalhar como um vírus e, embora isso normalmente seja bom, você ainda precisa ter isso em mente ao decidir onde iniciar a implementação em sua aplicação.

E estamos conversados...

"Porque todos devemos comparecer ante o tribunal de Cristo, para que cada um receba segundo o que tiver feito por meio do corpo, ou bem, ou mal."
2 Coríntios 5:10

Referências:


José Carlos Macoratti