Programação assíncrona

Introdução ao Async/Await no ASP.NET

Stephen Cleary

A maioria dos recursos online em torno do async/await supõem que você está desenvolvendo aplicações cliente, mas o async tem um lugar no servidor? A resposta é definitivamente “sim”. Este artigo é uma visão geral conceitual de solicitações assíncronas no ASP.NET, bem como uma referência para os melhores recursos online. Eu não vou abordar a sintaxe async ou await. Eu já fiz isso em uma postagem introdutória de blog (bit.ly/19IkogW) e em um artigo sobre práticas recomendadas do async (msdn.microsoft.com/magazine/jj991977). Este artigo se foca especificamente sobre a forma como o async funciona no ASP.NET.

Para aplicativos do cliente, como o Windows Store, Windows desktop e aplicativos do Windows Phone, o principal benefício é a capacidade de resposta assíncrona. Estes tipos de aplicativos usam async essencialmente para manter a interface do usuário com capacidade de resposta. Para aplicativos de servidor, o principal benefício do async é a escalabilidade. A chave para a escalabilidade do Node.js é a sua natureza inerentemente assíncrona. O Open Web Interface para .NET (OWIN) foi projetado desde o início para ser assíncrono e o ASP.NET também pode ser assíncrono. Async: Não é apenas para aplicativos de interface do usuário!

Manipulação de solicitação síncrona vs assíncrona

Antes de mergulhar em manipuladores de solicitação assíncrona, eu gostaria de rever brevemente como os manipuladores de solicitação síncrona trabalham no ASP.NET. Para este exemplo, vamos dizer que as solicitações no sistema dependem de algum recurso externo, como um banco de dados ou API da Web. Quando uma solicitação chega, o ASP.NET leva um de seus threads do pool de threads e atribui a essa solicitação. Como está escrito de forma síncrona, o manipulador de solicitação chamará esse recurso externo de forma síncrona. Isso bloqueia o thread de solicitação até a chamada para o retorno de recursos externos. A Figura 1 ilustra um pool de threads com dois threads, um dos quais está bloqueado à espera de um recurso externo.

Aguardando de forma síncrona por um recurso externo
Figura 1 Aguardando de forma síncrona por um recurso externo

Eventualmente, essas chamadas de recurso externo voltam e o thread de solicitação continua o processamento dessa solicitação. Quando a solicitação estiver completa e a resposta estiver pronta para ser enviada, o thread de solicitação é devolvido ao pool de threads.

Está tudo muito bem, até que seu servidor ASP.NET receba mais solicitações do que tem threads para manipular. Nesse ponto, as solicitações extras precisam esperar que um thread esteja disponível antes que possam ser executadas. A Figura 2 ilustra o mesmo servidor de dois threads quando recebe três solicitações.

A servidor de dois threads recebendo três solicitações
Figura 2 A servidor de dois threads recebendo três solicitações

Nessa situação, as duas primeiras solicitações são threads atribuídos do pool de threads. Cada uma dessas solicitações chama um recurso externo, bloqueando os seus threads. A terceira solicitação precisa esperar por um thread disponível antes que ele possa até mesmo iniciar o processamento, mas a solicitação já está no sistema. Seu temporizador está indo e está em perigo de um HTTP Erro 503 (Serviço não disponível).

Mas pense sobre isso por um segundo: Essa terceira solicitação está esperando por um thread, quando há outros dois threads no sistema de forma eficaz sem fazer nada. Esses threads estão apenas bloqueados à espera de uma chamada externa para retornar. Eles não estão fazendo nenhum trabalho real. Eles não estão em estado de execução e não recebem nenhum tempo de CPU. Esses threads estão apenas sendo desperdiçados, enquanto não há uma solicitação em necessidade. Essa é a situação abordada por solicitações assíncronas.

Os manipuladores de solicitação assíncrona operam de forma diferente. Quando uma solicitação chega, o ASP.NET leva um de seus threads do pool de threads e atribui a essa solicitação. Dessa vez, o manipulador de solicitação chamará esse recurso externo de forma assíncrona. Isso devolve o thread de solicitação para o pool de threads, até a chamada para o retorno de recursos externos. A Figura 3 ilustra o pool de threads com dois threads, enquanto a solicitação está aguardando de forma assíncrona por recurso externo.

Aguardando de forma assíncrona por um recurso externo
Figura 3 Aguardando de forma assíncrona por um recurso externo

A diferença importante é que o thread da solicitação foi devolvido ao pool de threads, enquanto a chamada assíncrona está em andamento. Enquanto o thread estiver no pool de threads, ele não estará mais associado a essa solicitação. Nesse momento, quando as devoluções de chamadas de recursos externos, o ASP.NET leva um de seus threads de pool e o atribui novamente a essa solicitação. Esse thread continua processando a solicitação. Quando a solicitação for concluída, esse thread é novamente devolvido ao pool de threads. Observe que, com manipuladores síncronos, o mesmo thread é usado para a duração da solicitação. Com manipuladores assíncronos, em contraste, os diferentes threads podem ser atribuídos à mesma solicitação (em momentos diferentes).

Agora, se três solicitações foram entrar, o servidor pode lidar facilmente. Como os threads são liberados para o pool de threads, sempre que a solicitação tem um trabalho assíncrono que está esperando, eles estão livres para lidar com novas solicitações, bem como as já existentes. As solicitações assíncronas permitem que um menor número de threads lidem com um número maior de solicitações. Por isso, o principal benefício do código assíncrono no ASP.NET é a escalabilidade.

Por que não aumentar o tamanho do pool de threads?

Nesse ponto, a questão é sempre perguntada: Porque não apenas aumentar o tamanho do pool de threads? A resposta é dupla: O código assíncrono expande tanto para mais longe como mais rápido do que o bloqueio aos threads do pool.

O código assíncrono pode expandir mais do que bloquear threads porque ele usa muito menos memória. Cada pool do thread em um sistema operacional moderno tem uma pilha de 1 MB, além de uma pilha do kernel não paginável. Isso não soa como muito até você começar a receber um monte de threads em seu servidor. Em contraste, a sobrecarga de memória para uma operação assíncrona é muito menor. Portanto, uma solicitação com uma operação assíncrona tem muito menos pressão de memória do que uma solicitação com um thread bloqueado. O código assíncrono permite que você use mais de sua memória para outras coisas (armazenamento em cache, por exemplo).

O código assíncrono pode expandir mais rápido do que o bloqueio de threads porque o pool de threads tem uma taxa de injeção limitada. Conforme isso é gravado, a taxa é de um thread a cada dois segundos. Esse limite de velocidade de injeção é uma coisa boa. Evita construção e destruição constante do thread. No entanto, considere o que acontece quando vem uma inundação repentina de solicitações. O código síncrono pode facilmente se atolar conforme as solicitações se utilizam dos threads disponíveis e as solicitações restantes têm que esperar para o pool de threads injetar novos threads. Por outro lado, um código assíncrono não necessita de um limite como esse, é “sempre visível", por assim dizer. O código assíncrono é mais sensível a oscilações bruscas no volume de solicitações.

Tenha em mente que o código assíncrono não substitui o pool de threads. Esse não é o pool de thread ou código assíncrono. É o pool de threads e código assíncrono. O código assíncrono permite sua aplicação para fazer o melhor uso do pool de threads. Ele leva o pool de threads existente e o transforma até 11.

O que ocorre com o thread que faz o trabalho assíncrono?

Eu me fiz essa pergunta o tempo todo. A implicação é que deve haver algum thread em algum lugar que está bloqueando a chamada de I/O para o recurso externo. Assim, o código assíncrono libera o thread da solicitação, mas apenas às custas de outro thread em outras partes do sistema, certo? Não, de forma nenhuma.

Para entender por que as solicitações assíncronas expandem, vou traçar um exemplo (simplificado) de chamada I/O assíncrona. Vamos dizer que uma solicitação precisa gravar em um arquivo. O thread de solicitação chama o método de gravação assíncrona. O WriteAsync é implementado pela Biblioteca de Classe Base (BCL), e usa portas de conclusão para o I/O assíncrono. Assim, a chamada WriteAsync é passada para o sistema operacional como um arquivo de gravação assíncrono. O sistema operacional se comunica com a pilha de drivers, passando ao longo dos dados para gravar em um pacote de solicitação de I/O (IRP).

Aqui é onde as coisas ficam interessantes: Se um driver de dispositivo não pode manipular um IRP imediatamente, ele deve manipulá-lo de forma assíncrona. Então, o driver diz ao disco para começar a gravar e retorna uma resposta “pendente” para o sistema operacional. O sistema operacional passa essa resposta “pendente” para o BCL e o BCL devolve uma tarefa incompleta para o código de manipulação de solicitação. O código de manipulação de solicitação aguarda a tarefa, que devolve uma tarefa incompleta desse método e assim por diante. Finalmente, o código de manipulação de solicitação acaba de devolver uma tarefa incompleta para o ASP.NET e o thread da solicitação é liberado para voltar para o pool de threads.

Agora, considere o estado atual do sistema. Existem várias estruturas de I/O que foram atribuídas (por exemplo, as instâncias de Tarefas e o IRP) e todas elas estão em um estado pendente/incompleto. No entanto, não há um thread que está bloqueado à espera de que a operação de gravação seja concluída. O ASP.NET, o BCL, o sistema operacional e o driver de dispositivo não têm uma linha dedicada ao trabalho assíncrono.

Quando o disco conclui a gravação dos dados, ele notifica o driver através de uma interrupção. O driver informa o sistema operacional que o IRP foi concluído e o sistema operacional notifica o BCL através da porta de conclusão. Um pool de thread responde a essa notificação ao concluir a tarefa que foi devolvida do WriteAsync que, por sua vez, retoma o código de solicitação assíncrona. Houve alguns threads "emprestados" para quantidades muito curtas de tempo durante esta fase de conclusão da notificação, mas nenhum thread foi realmente bloqueado enquanto a gravação estava em andamento.

Esse exemplo é drasticamente simplificado, mas passa pelo ponto principal: nenhum thread é necessário para o trabalho assíncrono verdadeiro. Nenhum tempo de CPU é necessário para realmente enviar os bytes por push. Há também uma lição secundária para aprender. Pense sobre o mundo do driver de dispositivo, como um driver de dispositivo deve lidar com uma IRP imediatamente ou de forma assíncrona. A manipulação síncrona não é uma opção. No nível de driver de dispositivo, todos os I/O não triviais são assíncronos. Muitos desenvolvedores têm um modelo mental que trata o "API natural" para operações I/O como síncronas, com a API assíncrona como uma camada criada sobre a API síncrona natural. No entanto, isso é completamente regressivo: na verdade, o API natural é assíncrona; e são as APIs síncronas que são implementadas usando o I/O assíncrono!

Por que os manipuladores assíncronos não estavam lá?

Se a manipulação da solicitação assíncrona é tão maravilhosa, por que já não estavam disponíveis? Na verdade, o código assíncrono é tão bom para escalabilidade que a plataforma ASP.NET apoiou manipuladores e módulos assíncronos desde os primórdios do Microsoft.NET Framework. As páginas da Web assíncronas foram introduzidas no ASP.NET 2.0 e o MVC tem controladores assíncronos no ASP.NET MVC 2.

No entanto, até recentemente, o código assíncrono tem sido sempre difícil de redigir e de difícil manutenção. Muitas empresas decidiram que era mais fácil apenas desenvolver o código de forma síncrona e pagar para farms de servidores maiores ou hospedagem mais cara. Agora, as coisas mudaram: no ASP.NET 4.5, o código assíncrono usando async e await é quase tão fácil como escrever código síncrono. Como grandes sistemas se movem em hospedagem em nuvem e exigem mais escala, mais e mais empresas estão adotando o async e await no ASP.NET.

O código assíncrono não é uma bala de prata

Por mais maravilhoso que a manipulação de solicitação assíncrona seja, não resolverá todos os seus problemas. Existem alguns equívocos comuns em torno do que o async e await pode fazer no ASP.NET.

Quando alguns desenvolvedores aprendem sobre async e await, eles acreditam que é uma maneira para que o código do servidor “renda" para o cliente (por exemplo, o navegador). No entanto, o async e await no ASP.NET apenas “rende" para o tempo de execução do ASP.NET. O protocolo HTTP permanece inalterado e você ainda tem apenas uma resposta por solicitação. Se você precisava de SignalR, AJAX ou UpdatePanel antes do async e await, você ainda precisa do SignalR, AJAX ou UpdatePanel após o async e await.

Manipulação de solicitação assíncrona com async e await pode ajudar na sua escala de aplicativos. No entanto, essa é a escala de um único servidor. Você ainda pode precisar planejar a expansão. Se você precisar de uma arquitetura de expansão, ainda precisa considerar solicitações sem monitoração de estado, idempotente e filas de confiança. O async e await ajudam um pouco: eles permitem que você tire o máximo proveito dos recursos do seu servidor, assim você não terá que expandir com tanta frequência. Mas se você precisar expandir, precisará de uma arquitetura distribuída adequada.

O async e await no ASP.NET são todos sobre I/O. Eles realmente se sobressaem em arquivos de leitura e de gravação, registros de banco de dados e APIs REST. No entanto, eles não são bons para tarefas vinculadas à CPU. Você pode começar algum trabalho de fundo ao aguardar o Task.Run, mas não há nenhum sentido em fazer isso. Na verdade, isso vai realmente prejudicar a sua escalabilidade, interferindo com as heurísticas do pool de thread do ASP.NET. Se você tem o trabalho vinculado à CPU para fazer no ASP.NET, a sua melhor aposta é apenas executá-lo diretamente no thread de solicitação. Como regra geral, não enfileire o trabalho ao pool de threads no ASP.NET.

Finalmente, considere a escalabilidade do sistema como um todo. Há uma década atrás, uma arquitetura comum era ter um servidor Web ASP.NET que conversou com um back-end de banco de dados do SQL Server. Nesse tipo de arquitetura simples, geralmente o servidor de banco de dados é o afunilamento da escalabilidade, não o servidor Web. Tornar as suas chamadas de banco de dados assíncronas provavelmente não ajudaria. Você poderia certamente usá-las para escalar o servidor Web, mas o servidor de banco de dados evitará o sistema como um todo a partir do ajuste de escala.

Rick Anderson argumenta contra as chamadas do banco de dados assíncronos em sua excelente postagem do blog, “E se minhas chamadas do banco de dados forem assíncronas?" (bit.ly/1rw66UB). Há dois argumentos que sustentam isso: primeiro, o código assíncrono é difícil (e, portanto, caro no tempo do desenvolvedor em comparação com apenas comprar servidores maiores). Segundo, o ajuste de escala do servidor Web não faz muito sentido se o back-end do banco de dados é o afunilamento. Ambos os argumentos fazem todo o sentido quando essa publicação foi escrita, mas ambos os argumentos têm enfraquecido ao longo do tempo. Primeiro, o código assíncrono é muito mais fácil de gravar com o async e await. Segundo, os dados dos back-ends para sites estão ajustando em escala conforme o mundo se move para a computação em nuvem. Os back-ends modernos terminam como banco de dados do Microsoft SQL Azure, NoSQL e outras APIs podem escalar muito mais do que um único SQL Server, empurrando o afunilamento de volta para o servidor Web. Nesse cenário, async/await pode trazer um enorme benefício ao dimensionar o ASP.NET.

Antes da Introdução

A primeira coisa que você precisa saber é que async e await são suportados apenas no ASP.NET 4.5. Há um pacote NuGet chamado Microsoft.Bcl.Async que permite o async e await para o .NET Framework 4, mas não os use. Não vai funcionar corretamente! A razão é que o próprio ASP.NET teve que mudar a forma como gerencia a sua manipulação de solicitação assíncrona para trabalhar melhor com o async e await. O pacote NuGet contém todos os tipos de necessidades do compilador, mas não vai corrigir o tempo de execução do ASP.NET. Não existe nenhuma solução. Você precisa do ASP.NET 4.5 ou superior.

Em seguida, esteja ciente de que o ASP.NET 4.5 introduz um “modo de quirks" no servidor. Se você criar um novo projeto ASP.NET 4.5, não precisará se preocupar. No entanto, se você atualizar um projeto existente para ASP.NET 4.5, todos os quirks serão ligados. Recomento desligar todos eles ao editar seu web.config e configurar httpRuntime.targetFramework para 4.5. Se seu aplicativo falhar com essa configuração (e você não quer gastar tempo para corrigir), é possível pelo menos obter o trabalho de async e await ao adicionar uma chave appSetting do aspnet:UseTaskFriendlySynchronizationContext com valor “verdadeiro”. A chave appSetting é desnecessária se você tem o httpRuntime.targetFramework definido para 4.5. A equipe de desenvolvimento na Web tem uma postagem de blog sobre os detalhes deste novo “modo quirks” em bit.ly/1pbmnzK. Dica: se você estiver vendo comportamento ou exceções estranhos e sua pilha de chamadas inclui LegacyAspNetSynchronizationContext, o aplicativo está sendo executado nesse modo quirk. O LegacyAspNetSynchronizationContext não é compatível com async. Você precisa do AspNetSynchronizationContext regular no ASP.NET 4.5.

No ASP.NET 4.5, todas as configurações do ASP.NET têm bons valores padrão para solicitações assíncronas, mas há um par de outras configurações que você pode querer mudar. A primeira é uma configuração IIS: considere o aumento do limite do IIS/fila HTTP.sys (Pools do aplicativo | Configurações Avançadas | Comprimento da Fila) do seu padrão de 1.000 para 5.000. O outro é uma configuração do tempo de execução do .NET: ServicePointManager.DefaultConnectionLimit, que tem um valor padrão de 12 vezes o número de núcleos. O DefaultConnectionLimit limita o número de conexões de saída simultâneas para o mesmo nome do host.

Uma palavra sobre as solicitações para abortar

Quando o ASP.NET processa uma solicitação de forma síncrona, tem um mecanismo muito simples para abortar uma solicitação (por exemplo, se a solicitação excedeu o tempo limite): Ele abortará o thread do trabalhador para essa solicitação. Isso faz sentido no mundo síncrono, onde cada solicitação tem o mesmo thread do trabalhador, do início ao fim. Abortar threads não é maravilhoso para a estabilidade a longo prazo do AppDomain. Portanto, por padrão, o ASP.NET reciclará regularmente seu aplicativo para manter as coisas limpas.

Com as solicitações assíncronas, o ASP.NET não abortará os threads do trabalhador, se ele quiser abortar uma solicitação. Em vez disso, ele cancelará a solicitação usando um CancellationToken. Os manipuladores de solicitação assíncrona devem aceitar e honrar os tokens de cancelamento. A maioria das estruturas mais recentes (incluindo API da Web, MVC e SignalR) criará e passará um CancellationToken diretamente. Tudo que você tem que fazer é declará-lo como um parâmetro. Você também pode acessar os tokens do ASP.NET diretamente. Por exemplo, HttpRequest.TimedOutToken é um CancellationToken que cancela quando a solicitação expira.

Como os aplicativos se movem para a nuvem, abortando solicitações que se tornam mais importantes. Aplicativos com base em nuvem são mais dependentes dos serviços externos que podem levar quantidades arbitrárias de tempo. Por exemplo, um modelo padrão é para repetir solicitações externas com retirada exponencial. Se seu aplicativo depende de vários serviços como esse, é uma boa ideia aplicar uma tampa de tempo limite para o processamento do pedido como um todo.

Estado atual de suporte assíncrono

Muitas bibliotecas foram atualizadas por questão de compatibilidade com async. O suporte assíncrono foi adicionado ao Entity Framework (no pacote EntityFramework NuGet) na versão 6. Você precisa ter cuidado para evitar o carregamento lento ao trabalhar de forma assíncrona. Porém, pelo fato de o carregamento lento, sempre é realizado de forma síncrona. O HttpClient (no pacote Microsoft.Net.Http NuGet) é um cliente HTTP moderno projetado com a async em mente, ideal para chamada API REST externa. É uma substituição moderna para HttpWebRequest e WebClient. A Biblioteca de armazenamento do Cliente Microsoft Azure (no pacote WindowsAzure.Storage NuGet) adicionou suporte assíncrono na versão 2.1.

As estruturas mais recentes, como API da Web e SignalR têm suporte completo para async e await. O API da Web, em especial, criou todo o seu pipeline em torno do suporte assíncrono: não apenas controladores assíncronos, mas filtros e manipuladores assíncronos também. A API da Web e SignalR tem uma história de async muito natural: você pode “apenas fazer isso” e isso “apenas funciona”.

Isso nos leva a uma história mais triste: Hoje, somente o ASP.NET MVC suporta parcialmente o async e await. O suporte básico está aqui - ações do controlador assíncrono e cancelamento do trabalho adequadamente. O site do ASP.NET tem tutorial absolutamente excelente sobre como usar as ações do controlador assíncrono no ASP.NET MVC (bit.ly/1m1LXTx). É o melhor recurso para iniciar com o async no MVC. Infelizmente, o ASP.NET MVC não suporta (atualmente) filtros assíncronos (bit.ly/1oAyHLc) ou ações filho assíncronas (bit.ly/1px47RG).

O ASP.NET na Web Forms é uma estrutura mais antiga, mas também tem suporte adequado para async e await. Novamente, o melhor recurso para iniciar é o tutorial no site ASP.NET para Web Forms assíncronos (bit.ly/Ydho7W). Com Web Forms, o suporte assíncrono é aceitável. Você precisa definir primeiro Page.Async para verdadeiro e então você pode usar PageAsyncTask para registrar o trabalho assíncrono com essa página (alternativamente, você pode usar os manipuladores de evento de anulação assíncrona). O PageAsyncTask também suporta cancelamento.

Se você tem um manipulador HTTP ou módulo HTTP personalizados, o ASP.NET agora suporta as versões assíncronas desses, também. Os manipuladores HTTP são suportados via HttpTaskAsyncHandler (bit.ly/1nWpWFj) e os módulos HTTP são suportados via EventHandlerTaskAsyncHelper (bit.ly/1m1Sn4O).

Conforme isso é gravado, a equipe ASP.NET está trabalhando em um novo projeto, conhecido como ASP.NET vNext. No vNext, todo o pipeline é assíncrono por padrão. Atualmente, o plano é combinar MVC e API da Web em uma única estrutura que tem suporte completo para async e await (incluindo filtros assíncronos e componentes de exibição assíncronos). Outras estruturas de async-ready, como o SignalR encontrarão um início natural em vNext. Na verdade, o futuro é assíncrono.

Respeite as redes de segurança

O ASP.NET 4.5 introduziu um par de novas “redes de segurança” que ajudam a detectar problemas assíncronos no seu aplicativo. Elas estão ativadas por padrão e devem permanecer assim.

Quando um manipulador síncrono tenta realizar um trabalho assíncrono, você receberá um InvalidOperationException com a mensagem: “Uma operação assíncrona não pode ser iniciada neste momento”. Existem duas causas principais para essa exceção. A primeira é quando uma página de Web Forms tem manipuladores assíncronos, mas esqueceu de definir Page.Async para verdadeiro. A segunda é quando o código síncrono chama um método de anulação assíncrona. Essa é mais uma razão para evitar a anulação assíncrona.

A outra rede de segurança é para manipuladores assíncronos: Quando um manipulador assíncrono conclui uma solicitação, mas o ASP.NET detecta trabalho assíncrono que não está concluído, você recebe um Invalid­OperationException com a mensagem: “Um módulo assíncrono ou manipulador concluiu enquanto uma operação assíncrona ainda está aguardando”. Geralmente, isso é devido ao código assíncrono chamando um método inválido assíncrono, mas ele também pode ser causado pelo uso inadequado de um componente do Padrão Assíncrono com base no Evento (EAP) (bit.ly/19VdUWu).

Existe uma opção que você pode usar para desligar ambas as redes de segurança: HttpContext.AllowAsyncDuringSyncStages (ele também pode ser definido no web.config). Algumas páginas na Internet sugerem a configuração disso sempre que você ver essas exceções. Eu não posso discordar com mais veemência. Sério, eu não sei por que isso é possível. Desabilitar as redes de segurança é uma ideia horrível. A única razão possível que eu posso pensar é se o seu código já está fazendo alguma coisa assíncrona extremamente avançada (além de qualquer coisa que você já tentou) e você é um gênio de multithreading. Então, se você leu todo este artigo bocejando e pensando: “Por favor, eu não sou bobo”, então eu suponho que você pode considerar a desabilitação das redes de segurança. Para o resto de nós, isso é uma opção extremamente perigosa e não deve ser definida, a menos que você esteja totalmente ciente das ramificações.

Introdução

Finalmente! Pronto para começar a aproveitar o async e await? Eu agradeço sua paciência.

Primeiro, reveja a seção “O código assíncrono não é uma bala de prata” nesse artigo para garantir que async/await seja benéfica para sua arquitetura. Em seguida, atualize seu aplicativo para ASP.NET 4.5 e desligue o modo de interpretação (não é uma má ideia executá-lo a este ponto, apenas para se certificar de que nada quebre). Nesse ponto, você está pronto para começar o verdadeiro trabalho async/await.

Comece nas “folhas”. Pense em como suas solicitações são processadas e identifique quaisquer operações com base em I/O, especialmente qualquer coisa com base na rede. Os exemplos comuns são consultas a banco de dados e comandos e chamadas para outros serviços da Web e APIs. Escolha um para iniciar e faça um pouco de pesquisa para localizar a melhor opção para realizar essa operação usando async/await. Muitos dos tipos de BCL integrados agora são de async-ready no .NET Framework 4.5. Por exemplo, o SmtpClient tem o método SendMailAsync. Alguns tipos têm as substituições de async-ready disponíveis. Por exemplo, HttpWebRequest e WebClient podem ser substituídos por HttpClient. Atualize suas versões de bibliotecas, se necessário. Por exemplo, Entity Framework tem métodos async compatíveis no EF6.

No entanto, evite “assincronia falsa” nas bibliotecas. A assincronia falsa é quando um componente tem um API de async-ready, mas é implementada apenas envolvendo a API síncrona dentro de um thread do pool de thread. Isso é contraproducente para a escalabilidade no ASP.NET. Um exemplo proeminente da assincronia falsa é Newtonsoft JSON.NET, um caso diverso da excelente biblioteca. É melhor não chamar as versões assíncronas (falsas) para serialização JSON. Apenas chame as versões síncronas no lugar. Um exemplo mais difícil de assincronia falsa são os fluxos do arquivo BCL. Quando um fluxo de arquivo está aberto, ele deve ser explicitamente aberto para acesso assíncrono. Caso contrário, ele usará assincronia falsa, bloqueando sincronicamente um thread do pool de thread nas leituras e gravações do arquivo.

Uma vez que escolheu uma “folha”, comece com um método em seu código que chama até essa API e o transforma em um método Async que chama a API async-ready via await. Se o API que você chamou suporta CancellationToken, seu método deve ter um CancellationToken e passá-lo para o método API.

Sempre que você marcar um método assíncrono, você deve mudar seu tipo de retorno: void se torna Task e um tipo non-void T se torna Task<T>. Você verá que, em seguida, todos os chamadores deste método precisam se tornar async para que eles possam await a tarefa e assim por diante. Além disso, acrescente Async ao nome de seu método para seguir com as convenções de Padrão Assíncrono com base na Tarefa (bit.ly/1uBKGKR).

Permita que o padrão async/await cresça a pilha de chamada em direção ao “tronco”. No tronco, seu código interagirá com o ASP.NET Framework (MVC, Web Forms, API da Web). Leia o tutorial apropriado na seção “Estado Atual do Suporte Async” anteriormente nesse artigo para integrar seu código assíncrono com a estrutura.

Ao longo do caminho, identifique qualquer estado de thread local. Como as solicitações assíncronas podem mudar os threads, o estado do thread local como ThreadStaticAttribute, ThreadLocal<T>, slots de dados de thread e CallContext.GetData/SetData não funcionarão. Substitua com HttpContext.Items, se possível, ou você pode armazenar dados imutáveis em CallContext.LogicalGetData/LogicalSetData.

Aqui está uma dica que eu achei útil: você pode (temporariamente) duplicar seu código para criar uma partição vertical. Com essa técnica, você não pode mudar seus métodos síncronos para assíncronos. Você copia todo o método síncrono e muda a cópia para ser assíncrona. Você pode manter a maior parte de seu aplicativo usando os métodos síncronos e apenas criar um corte vertical pequeno de assincronia. Isso é ótimo se você quiser explorar o Async como uma verificação de conceito ou fazer o teste de carga ou apenas parte do aplicativo para obter uma sensação de como seu sistema pode escalar. Você pode ter uma solicitação (ou página) que é completamente assíncrona, enquanto o resto do seu aplicativo permanece síncrono. Claro, você não quer manter duplicatas para cada um dos seus métodos. Eventualmente, todo o código de I/O associado será assíncrono e as cópias síncronas podem ser removidas.

Conclusão

Eu espero que este artigo tenha ajudado a obter um fundamento conceitual nas solicitações assíncronas no ASP.NET. Usar o Async e aguardar é mais fácil do que nunca para gravar aplicativos Web, serviços e APIs que fazem uso máximo de seus recursos do servidor. Async é incrível!


Stephen Cleary é marido, pai e programador que mora no norte de Michigan. Ele já trabalhou com multithreading e programação assíncrona por 16 anos e tem usado suporte assíncrono no Microsoft .NET Framework desde a primeira visualização da tecnologia da comunidade. Seu site, incluindo seu blog, é stephencleary.com.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: James McCaffrey