Usando métodos assíncronos no ASP.NET MVC 4

por Rick Anderson

Este tutorial ensinará os conceitos básicos da criação de um aplicativo Web assíncrono ASP.NET MVC usando o Visual Studio Express 2012 para Web, que é uma versão gratuita do Microsoft Visual Studio. Você também pode usar o Visual Studio 2012.

Um exemplo completo é fornecido para este tutorial no github https://github.com/RickAndMSFT/Async-ASP.NET/

O ASP.NET classe controlador MVC 4 em combinação .NET 4.5 permite que você escreva métodos de ação assíncrona que retornam um objeto do tipo Task<ActionResult>. O .NET Framework 4 introduziu um conceito de programação assíncrona conhecido como Tarefa e ASP.NET MVC 4 dá suporte à Tarefa. As tarefas são representadas pelo Tipo de tarefa e tipos relacionados no namespace System.Threading.Tasks . O .NET Framework 4.5 se baseia nesse suporte assíncrono com as palavras-chave await e async que tornam o trabalho com objetos Task muito menos complexo do que as abordagens assíncronas anteriores. A palavra-chave await é uma abreviação sintática para indicar que um pedaço de código deve aguardar de forma assíncrona algum outro trecho de código. O palavra-chave assíncrono representa uma dica que você pode usar para marcar métodos como métodos assíncronos baseados em tarefa. A combinação de await, asynce task object torna muito mais fácil para você escrever código assíncrono no .NET 4.5. O novo modelo para métodos assíncronos é chamado de TAP (Padrão Assíncrono baseado em tarefa). Este tutorial pressupõe que você tenha alguma familiaridade com a programação assíncrona usando palavras-chave await e async e o namespace Task .

Para obter mais informações sobre como usar palavras-chave await e async e o namespace Task , consulte as referências a seguir.

Como as solicitações são processadas pelo pool de threads

No servidor Web, o .NET Framework mantém um pool de threads que são usados para atender a solicitações de ASP.NET. Quando uma solicitação chega, um thread do pool é expedido para processar essa solicitação. Se a solicitação for processada de forma síncrona, o thread que processa a solicitação estará ocupado enquanto a solicitação estiver sendo processada e esse thread não poderá atender a outra solicitação.

Isso pode não ser um problema, pois o pool de threads pode ser grande o suficiente para acomodar muitos threads ocupados. No entanto, o número de threads no pool de threads é limitado (o máximo padrão para .NET 4.5 é 5.000). Em aplicativos grandes com alta simultaneidade de solicitações de execução longa, todos os threads disponíveis podem estar ocupados. Essa condição é conhecida como fome de thread. Quando essa condição é atingida, o servidor Web enfileira solicitações. Se a fila de solicitações ficar cheia, o servidor Web rejeitará solicitações com um status HTTP 503 (Servidor Muito Ocupado). O pool de threads CLR tem limitações em novas injeções de thread. Se a simultaneidade for de intermitência (ou seja, seu site poderá receber um grande número de solicitações) e todos os threads de solicitação disponíveis estiverem ocupados devido a chamadas de back-end com alta latência, a taxa limitada de injeção de thread pode fazer com que seu aplicativo responda muito mal. Além disso, cada novo thread adicionado ao pool de threads tem sobrecarga (como 1 MB de memória de pilha). Um aplicativo Web que usa métodos síncronos para atender a chamadas de alta latência em que o pool de threads cresce para o máximo padrão do .NET 4.5 de 5.000 threads consumiria aproximadamente 5 GB a mais de memória do que um aplicativo capaz de atender às mesmas solicitações usando métodos assíncronos e apenas 50 threads. Quando você está fazendo um trabalho assíncrono, nem sempre está usando um thread. Por exemplo, quando você fizer uma solicitação de serviço Web assíncrona, ASP.NET não usará threads entre a chamada do método assíncrono e a espera. Usar o pool de threads para atender a solicitações com alta latência pode levar a um grande volume de memória e à utilização ruim do hardware do servidor.

Processando solicitações assíncronas

Em um aplicativo Web que vê um grande número de solicitações simultâneas na inicialização ou tem uma carga com intermitência (em que a simultaneidade aumenta repentinamente), fazer chamadas de serviço Web assíncronas aumenta a capacidade de resposta do aplicativo. Uma solicitação assíncrona leva o mesmo tempo para ser processada como uma solicitação síncrona. Se uma solicitação fizer uma chamada de serviço Web que requer dois segundos para ser concluída, a solicitação levará dois segundos, seja ela executada de forma síncrona ou assíncrona. No entanto, durante uma chamada assíncrona, um thread não é impedido de responder a outras solicitações enquanto aguarda a conclusão da primeira solicitação. Portanto, solicitações assíncronas impedem o crescimento da fila de solicitações e do pool de threads quando há muitas solicitações simultâneas que invocam operações de execução longa.

Escolhendo métodos de ação síncrona ou assíncrona

Esta seção lista as diretrizes de quando usar métodos de ação síncronos ou assíncronos. Estas são apenas diretrizes; examine cada aplicativo individualmente para determinar se os métodos assíncronos ajudam com o desempenho.

Em geral, use métodos síncronos para as seguintes condições:

  • As operações são simples ou de execução curta.
  • A simplicidade é mais importante do que a eficiência.
  • As operações são principalmente operações de CPU em vez de operações que envolvem sobrecarga extensiva de disco ou rede. O uso de métodos de ação assíncrona em operações associadas à CPU não oferece benefícios e resulta em mais sobrecarga.

Em geral, use métodos assíncronos para as seguintes condições:

  • Você está chamando serviços que podem ser consumidos por meio de métodos assíncronos e está usando o .NET 4.5 ou superior.
  • As operações são associadas à rede ou associadas à E/S em vez de associadas à CPU.
  • O paralelismo é mais importante do que a simplicidade do código.
  • Você deseja fornecer um mecanismo que permita que os usuários cancelem uma solicitação de execução prolongada.
  • Quando o benefício de alternar threads supera o custo da opção de contexto. Em geral, você deverá tornar um método assíncrono se o método síncrono aguardar o thread de solicitação ASP.NET enquanto não faz nenhum trabalho. Ao tornar a chamada assíncrona, o thread de solicitação de ASP.NET não fica parado sem trabalho enquanto aguarda a conclusão da solicitação do serviço Web.
  • O teste mostra que as operações de bloqueio são um gargalo no desempenho do site e que o IIS pode atender a mais solicitações usando métodos assíncronos para essas chamadas de bloqueio.

O exemplo baixável mostra como usar métodos de ação assíncrona com eficiência. O exemplo fornecido foi projetado para fornecer uma demonstração simples de programação assíncrona no ASP.NET MVC 4 usando o .NET 4.5. O exemplo não se destina a ser uma arquitetura de referência para programação assíncrona no ASP.NET MVC. O programa de exemplo chama ASP.NET Web API métodos que, por sua vez, chamam Task.Delay para simular chamadas de serviço Web de longa execução. A maioria dos aplicativos de produção não mostrará esses benefícios óbvios ao usar métodos de ação assíncrona.

Poucos aplicativos exigem que todos os métodos de ação sejam assíncronos. Muitas vezes, converter alguns métodos de ação síncrona em métodos assíncronos fornece o melhor aumento de eficiência para a quantidade de trabalho necessária.

O aplicativo de exemplo

Você pode baixar o aplicativo de exemplo de https://github.com/RickAndMSFT/Async-ASP.NET/ no site do GitHub . O repositório consiste em três projetos:

  • Mvc4Async: o projeto ASP.NET MVC 4 que contém o código usado neste tutorial. Ele faz chamadas à API Web para o serviço WebAPIpgw .
  • WebAPIpgw: o ASP.NET projeto de API Web MVC 4 que implementa os Products, Gizmos and Widgets controladores. Ele fornece os dados para o projeto WebAppAsync e o projeto Mvc4Async .
  • WebAppAsync: o projeto ASP.NET Web Forms usado em outro tutorial.

O método de ação síncrona do Gizmos

O código a seguir mostra o Gizmos método de ação síncrona usado para exibir uma lista de aparelhos. (Para este artigo, um gizmo é um dispositivo mecânico fictício.)

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}

O código a seguir mostra o GetGizmos método do serviço de gizmo.

public class GizmoService
{
    public async Task<List<Gizmo>> GetGizmosAsync(
        // Implementation removed.
       
    public List<Gizmo> GetGizmos()
    {
        var uri = Util.getServiceUri("Gizmos");
        using (WebClient webClient = new WebClient())
        {
            return JsonConvert.DeserializeObject<List<Gizmo>>(
                webClient.DownloadString(uri)
            );
        }
    }
}

O GizmoService GetGizmos método passa um URI para um serviço HTTP ASP.NET Web API que retorna uma lista de dados de gizmos. O projeto WebAPIpgw contém a implementação da API gizmos, widget Web e product dos controladores.
A imagem a seguir mostra a exibição de gizmos do projeto de exemplo.

Aparelhos

Criando um método de ação de gizmos assíncrono

O exemplo usa as novas palavras-chave async e await (disponíveis no .NET 4.5 e no Visual Studio 2012) para permitir que o compilador seja responsável por manter as transformações complicadas necessárias para a programação assíncrona. O compilador permite escrever código usando os constructos de fluxo de controle síncrono do C#e o compilador aplica automaticamente as transformações necessárias para usar retornos de chamada para evitar o bloqueio de threads.

O código a seguir mostra o Gizmos método síncrono e o método assíncrono GizmosAsync . Se o navegador der suporte ao elemento HTML 5<mark>, você verá as alterações no GizmosAsync realce amarelo.

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}
public async Task<ActionResult> GizmosAsync()
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", await gizmoService.GetGizmosAsync());
}

As alterações a seguir foram aplicadas para permitir que o GizmosAsync seja assíncrono.

  • O método é marcado com o palavra-chave assíncrono, que informa ao compilador para gerar retornos de chamada para partes do corpo e criar automaticamente um Task<ActionResult> que é retornado.
  • "Async" foi acrescentado ao nome do método. Acrescentar "Async" não é necessário, mas é a convenção ao escrever métodos assíncronos.
  • O tipo de retorno foi alterado de ActionResult para Task<ActionResult>. O tipo de retorno de Task<ActionResult> representa o trabalho contínuo e fornece aos chamadores do método um identificador por meio do qual aguardar a conclusão da operação assíncrona. Nesse caso, o chamador é o serviço Web. Task<ActionResult> representa o trabalho contínuo com um resultado de ActionResult.
  • O palavra-chave de espera foi aplicado à chamada de serviço Web.
  • A API de serviço Web assíncrona foi chamada (GetGizmosAsync).

Dentro do corpo do GetGizmosAsync método, outro método assíncrono é GetGizmosAsync chamado. GetGizmosAsync retorna imediatamente um Task<List<Gizmo>> que eventualmente será concluído quando os dados estiverem disponíveis. Como você não deseja fazer mais nada até ter os dados do gizmo, o código aguarda a tarefa (usando a palavra-chave await). Você pode usar a palavra-chave await somente em métodos anotados com o palavra-chave assíncrono.

A palavra-chave await não bloqueia o thread até que a tarefa seja concluída. Ele inscreve o restante do método como um retorno de chamada na tarefa e retorna imediatamente. Quando a tarefa aguardada eventualmente for concluída, ela invocará esse retorno de chamada e, portanto, retomará a execução do método exatamente de onde parou. Para obter mais informações sobre como usar as palavras-chave await e async e o namespace Task , consulte as referências assíncronas.

O código a seguir mostra os métodos GetGizmos e GetGizmosAsync.

public List<Gizmo> GetGizmos()
{
    var uri = Util.getServiceUri("Gizmos");
    using (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            webClient.DownloadString(uri)
        );
    }
}
public async Task<List<Gizmo>> GetGizmosAsync()
{
    var uri = Util.getServiceUri("Gizmos");
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

As alterações assíncronas são semelhantes às feitas no GizmosAsync acima.

  • A assinatura do método foi anotada com o palavra-chave assíncrono, o tipo de retorno foi alterado para Task<List<Gizmo>>e Async foi acrescentado ao nome do método.
  • A classe HttpClient assíncrona é usada em vez da classe WebClient .
  • O palavra-chave de espera foi aplicado aos métodos assíncronos HttpClient.

A imagem a seguir mostra o modo de exibição de gizmo assíncrono.

Async

A apresentação de navegadores dos dados de gizmos é idêntica à exibição criada pela chamada síncrona. A única diferença é que a versão assíncrona pode ter um desempenho maior em cargas pesadas.

Executando várias operações em paralelo

Os métodos de ação assíncrona têm uma vantagem significativa em relação aos métodos síncronos quando uma ação deve executar várias operações independentes. No exemplo fornecido, o método PWGsíncrono (para Produtos, Widgets e Gizmos) exibe os resultados de três chamadas de serviço Web para obter uma lista de produtos, widgets e gizmos. O projeto ASP.NET Web API que fornece esses serviços usa Task.Delay para simular latência ou chamadas de rede lentas. Quando o atraso é definido como 500 milissegundos, o método assíncrono PWGasync leva pouco mais de 500 milissegundos para ser concluído, enquanto a versão síncrona PWG leva mais de 1.500 milissegundos. O método síncrono PWG é mostrado no código a seguir.

public ActionResult PWG()
{
    ViewBag.SyncType = "Synchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );

    return View("PWG", pwgVM);
}

O método assíncrono PWGasync é mostrado no código a seguir.

public async Task<ActionResult> PWGasync()
{
    ViewBag.SyncType = "Asynchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var widgetTask = widgetService.GetWidgetsAsync();
    var prodTask = prodService.GetProductsAsync();
    var gizmoTask = gizmoService.GetGizmosAsync();

    await Task.WhenAll(widgetTask, prodTask, gizmoTask);

    var pwgVM = new ProdGizWidgetVM(
       widgetTask.Result,
       prodTask.Result,
       gizmoTask.Result
       );

    return View("PWG", pwgVM);
}

A imagem a seguir mostra a exibição retornada do método PWGasync .

pwgAsync

Usando um token de cancelamento

Os métodos de ação assíncrona que retornam Task<ActionResult>são canceláveis, ou seja, eles assumem um parâmetro CancellationToken quando um é fornecido com o atributo AsyncTimeout . O código a seguir mostra o GizmosCancelAsync método com um tempo limite de 150 milissegundos.

[AsyncTimeout(150)]
[HandleError(ExceptionType = typeof(TimeoutException),
                                    View = "TimeoutError")]
public async Task<ActionResult> GizmosCancelAsync(
                       CancellationToken cancellationToken )
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos",
        await gizmoService.GetGizmosAsync(cancellationToken));
}

O código a seguir mostra a sobrecarga GetGizmosAsync, que usa um parâmetro CancellationToken .

public async Task<List<Gizmo>> GetGizmosAsync(string uri,
    CancellationToken cancelToken = default(CancellationToken))
{
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri, cancelToken);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

No aplicativo de exemplo fornecido, selecionar o link Demonstração de Token de Cancelamento chama o GizmosCancelAsync método e demonstra o cancelamento da chamada assíncrona.

Configuração do servidor para chamadas de serviço Web de alta simultaneidade/alta latência

Para obter os benefícios de um aplicativo Web assíncrono, talvez seja necessário fazer algumas alterações na configuração de servidor padrão. Tenha o seguinte em mente ao configurar e testar por estresse seu aplicativo Web assíncrono.

  • O Windows 7, o Windows Vista e todos os sistemas operacionais cliente Windows têm no máximo 10 solicitações simultâneas. Você precisará de um sistema operacional Windows Server para ver os benefícios dos métodos assíncronos sob alta carga.

  • Registre o .NET 4.5 com o IIS em um prompt de comandos com privilégios elevados:
    %windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis -i
    Consulte ASP.NET Ferramenta de Registro do IIS (Aspnet_regiis.exe)

  • Talvez seja necessário aumentar o limite de fila HTTP.sys do valor padrão de 1.000 para 5.000. Se a configuração for muito baixa, você poderá ver HTTP.sys rejeitar solicitações com um status HTTP 503. Para alterar o limite da fila HTTP.sys:

    • Abra o gerenciador do IIS e navegue até o painel Pools de Aplicativos.
    • Clique com o botão direito do mouse no pool de aplicativos de destino e selecione Configurações Avançadas.
      Avançado
    • Na caixa de diálogo Configurações Avançadas , altere Comprimento da Fila de 1.000 para 5.000.
      Tamanho da fila

    Observe que, nas imagens acima, o .NET Framework é listado como v4.0, mesmo que o pool de aplicativos esteja usando o .NET 4.5. Para entender essa discrepância, confira o seguinte:

  • Se o aplicativo estiver usando serviços Web ou System.NET para se comunicar com um back-end por HTTP, talvez seja necessário aumentar o elemento connectionManagement/maxconnection . Para aplicativos ASP.NET, isso é limitado pelo recurso autoConfig a 12 vezes o número de CPUs. Isso significa que, em um quad-proc, você pode ter no máximo 12 * 4 = 48 conexões simultâneas com um ponto de extremidade ip. Como isso está vinculado à autoConfig, a maneira mais fácil de aumentar maxconnection em um aplicativo ASP.NET é definir System.Net.ServicePointManager.DefaultConnectionLimit programaticamente no método from Application_Start no arquivo global.asax . Consulte o download de exemplo para obter um exemplo.

  • No .NET 4.5, o padrão de 5000 para MaxConcurrentRequestsPerCPU deve ser bom.