O documento discute programação assíncrona com C# e fornece exemplos de como usar o padrão Task-based Asynchronous para escrever código assíncrono de forma eficiente. Ele compara diferentes abordagens como APM, EAP e TAP e demonstra como usar recursos como async, await e Task.Run para evitar bloqueios de threads.
Sempre que havia um pedido em uma mesa, o garçom o anotava, ia até a cozinha, o entregava ao pizzaiolo e ficava lá, esperando a pizza assar
Depois de pronta, o garçom saía da cozinha e entregava a pizza na mesa e por fim, voltava ao balcão para esperar novos pedidos
O gerente percebeu que se houvessem mais que cinco pedidos sendo preparados, os próximos clientes não conseguiam fazer seus pedidos e iam embora
Como ele era muito esperto, resolveu contratar mais cinco garçons para resolver o problema
Desta forma, havia um garçom para cada mesa
Como ele era muito esperto, resolveu contratar mais cinco garçons para resolver o problema
Desta forma, havia um garçom para cada mesa
Mas devido a contratação dos novos garçons, o custo operacional da pizzaria subiu muito, gerando prejuízos no final do mês
Para cobrir estes gastos, o gerente resolveu aumentar o preço das pizzas, o que espantou a clientela, que passou a frequentar a Pizzeria Veloce do outro lado da rua
A Pizzeria Ritardato fechou pouco tempo depois
Na Pizzeria Veloce, haviam três garçons e dez mesas
Sempre que havia um pedido em uma mesa, o garçom o anotava, ia até a cozinha, entregava ao pizzaiolo e voltava imediatamente ao balcão para esperar novos pedidos
Quando uma pizza estava pronta, o pizzaiolo tocava uma campainha
Um garçom que não estivesse ocupado ia até a cozinha, pegava a pizza e entregava na mesa que fez o pedido
Não era necessariamente o mesmo garçom que anotava e entregava o pedido em uma mesa
Com o fechamento da Pizzeria Ritardato, o movimento de cliente da Pizzeria Veloce aumentou
O gerente observou que da forma que trabalhavam, três garçons eram mais que o suficiente para atender as dez mesas
Por isso, resolveu ampliar o espaço para quinze mesas, mantendo o mesmo número de garçons
Desta forma, os lucros aumentaram, com o crescimento das vendas sem o aumento do custo com funcionários
Assim, a Pizzeria Veloce aumentou sua eficiência, utilizando o mesmo número de recursos para vender mais
Por que a Pizzeria Veloce conseguiu ser mais eficiente que a Pizzeria Ritardato?
Porque funcionava de forma assíncrona!
A principal ideia por traz da programação assíncrona é utilizar de forma eficiente os recursos computacionais
Consiste basicamente em não bloquear uma thread enquanto esta espera por um resultado de um processamento lento (I/O)
Uma thread bloqueada gasta tempo do processador
Tempo do processador = energia elétrica = dinheiro
Energia elétrica = natureza
A cada Thread.Sleep, duas espécies entram em extinção na Amazônia
Ao fazer uma requisição externa, a thread volta para o pool para atender outras requisições
Quando o processamento externo termina, uma notificação é gerada para que uma thread (pode ser outra) receba o resultado e continue o processamento
A Pizzeria Ritardato usava mais threads que a Veloce e exatamente por isso era menos eficiente
O objetivo é otimizar a programação paralela.
Na analogia com as pizzarias.
Memória total do servidor: Orçamento da Pizzaria
Serviços externos: Banco de dados, API HTTP, disco, etc.
Existem três padrões possíveis para programação assíncrona no .NET:
Asynchronous Programming Model (APM) – Utiliza a interface IAsyncResult, com métodos com prefixo Begin e End para a requisição e resultado. Obsoleto.
Event-based Asynchronous Pattern (EAP) – Requer um método para a requisição e um evento para o resultado. Obsoleto.
Task-based Asynchronous Pattern (TAP) – Utiliza o tipo Task<T> requer apenas um método. Recomendado.
É o padrão recomendado.
Baseia-se no uso do tipo Task ou Task<T>, onde T é o resultado do processamento da tarefa.
Existe desde o C# 4.
Uma Task também é chamada de Promisse ou Future (termo da teoria da computação) – Execução em potencial de uma rotina computacional
A rotina a ser executada é passada através de uma expressão lambda. Delegates Action ou Func
A execução é feita por uma thread do threadpool do .NET
O tamanho do threadpool é limitado pela memória, mas existe um limite por processo
Apesar de utilizar duas threads (uma principal e uma para a Task), o exemplo anterior funciona de forma síncrona, pois o processamento da primeira thread é bloqueado na chamada task.Result
Interface não é responsiva.
Seria melhor se conseguíssemos ser notificados da conclusão da tarefa e só depois disso processar o resultado.
A classe Task permite o encadeamento de tarefas através da definição da continuação, pelo método ContinueWith.
Desta forma, é possível evitar o bloqueio da thread principal enquanto se espera o resultado.
Se for uma aplicação gráfica, a thread da interface não fica bloqueada pela ação definida na tarefa.
Mas se o método GetName estiver realizando alguma operação de I/O, ainda existe o bloqueio da thread que está executando a tarefa (do threadpool), o que torna a aplicação ineficiente.
Um outro problema é que o encadeamento de continuações tende a deixar o código confuso.
E por último, é necessário sincronizar o contexto manualmente em aplicações gráficas, já que o ContinueWith é executado por uma thread do pool, e por isso, não tem acesso aos componentes da interface.
E se fossem para buscar vários nomes?
No exemplo 1 ficaria assim
No exemplo 2, seria necessário recursão para garantir que as tarefas acontecam em sequencia.
Com o objetivo de simplificar o desenvolvimento deste tipo de aplicação, o C# 5.0 incluiu duas novas palavras-chave: async e await
Marcador que permite o uso de outra palavra-chave no método -> AWAIT
É aqui onde a mágica acontece.
Permite “esperarmos” SEM BLOQUEIO a conclusão de uma Task
Como resolver os problemas do nosso exemplo com estas palavras-chave?
Uma implementação assíncrona do método GetName, que utilize uma API assíncrona do .NET (por exemplo, HttpClient) e retorne uma Task
Alterar o event handler do botão, incluindo a palavra-chave async, que nos permite utilizar a outra palavra-chave await
Implementação limpa e mais simples.
Versão com repetição do mesmo código
O que a palavra-chave async faz?
Basicamente, ela nos permite usar no corpo do método a palavra-chave await
O compilador busca nos métodos marcados como async pelo uso da palavra-chave await.
Estes pontos são como “quebras de página” de um texto, onde os métodos são separados em várias rotinas, representadas dentro de uma máquina de estados.
Esta máquina recebe as notificações de conclusão das Tasks avança um estado sempre que isso ocorre.
Para o desenvolvedor, isso é transparente: A impressão é que tudo acontece de forma linear, mas na prática, o código gerado pelo compilador é complexo e adiciona um certo overhead à execução.
Código refatorado pelo compilador.
Máquina de estados gerada para o método.
Não valeria a pena, na perspectiva do uso de recursos, utilizar programação assíncrona com C#, devido ao overhead gerado para coordenar a execução deste tipo de código, se não houvesse o ganho que as notificações que estas APIs provêm.
APIs verdadeiramente assíncronas utilizam Completion Ports do sistema operacional para receber as notificações do resultado do processamento de uma requisição de I/O.
Se não houvesse este recurso, seria necessário uma outra thread para realizar o polling e monitorar o resultado, o que tornaria o processo assíncrono tão ineficiente quanto a programação síncrona
As classes do .NET que expõem uma API assíncrona (TcpClient, HttpClient, etc.) contam com este recurso.
Ou implemente a sua utilizando o TaskCompletionSource
Por este motivo, deve-se evitar utilizar async wrappers, a não ser que o objetivo seja apenas deixar a interface gráfica (UI) mais responsiva
Wrapper para um método síncrono não traz ganhos de eficiência.
São úteis apenas em interfaces gráficas, mas se houver uma implementação assíncrona, deve ser preferida.
Implementação assíncrona que usa os métodos GetAsync e ReadAsStringAsync do .NET
Métodos assíncronos sempre retornam Task e tem um sufixo Async
Não adianta transformar apenas parte do seu código em assíncrono. Se houver qualquer bloqueio, o desempenho pode ser pior do que a versão síncrona, devido ao overhead.
Toda as APIs que fazem IO e bloqueiam envolvidas na chamada devem ser assíncronas.
Todo o callstack da chamada deve ser transformado.
Metodo síncrono do Web API
Versão assíncrono do Web API
Basta retornar um Task<T>
Post-fixo Async no nome do método
Retornar sempre Task ou Task<T>. Não usar async void em métodos.
ConfigureAwait(false) evita a captura do contexto da requisição para a continuação, para evitar problemas com deadlocks.
Como as chamadas não ocorrem de forma linear, não existe um callstack disponível semelhante a do código síncrono, o que dificulta o debug do código legado.
O ideal é logar todas as chamadas.