Compartilhar via


Uso de métodos assíncronos no ASP.NET 4.5

por Rick Anderson

Este tutorial ensinará os conceitos básicos da criação de um aplicativo de ASP.NET Web Forms assíncrono 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. As seções a seguir estão incluídas neste tutorial.

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

ASP.NET Páginas da Web 4.5 na combinação .NET 4.5 permite registrar métodos assíncronos que retornam um objeto do tipo Task. O .NET Framework 4 introduziu um conceito de programação assíncrona chamado task e ASP.NET 4.5 dá suporte a Task. 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 assíncronas que tornam o trabalho com objetos Task muito menos complexo do que as abordagens assíncronas anteriores. O palavra-chave de espera é uma abreviação sintática para indicar que um pedaço de código deve aguardar de forma assíncrona algum outro 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, async e o objeto Task facilita muito a gravação de 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 assíncronas e o namespace Tarefa .

Para obter mais informações sobre o uso de 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 é enviado 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 longa execução, todos os threads disponíveis podem estar ocupados. Essa condição é conhecida como fome de thread. Quando essa condição é atingida, o servidor Web faz solicitações. Se a fila de solicitações ficar cheia, o servidor Web rejeitará as 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 estiver intermitida (ou seja, seu site da Web poderá receber um grande número de solicitações de repente) 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 usando 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. O uso do pool de threads para solicitações de serviço com alta latência pode levar a um grande volume de memória e má utilização do hardware do servidor.

Processando solicitações assíncronas

Em aplicativos Web que veem um grande número de solicitações simultâneas na inicialização ou têm uma carga de intermitência (em que a simultaneidade aumenta repentinamente), tornar as chamadas de serviço Web assíncronas aumentará a capacidade de resposta do seu aplicativo. Uma solicitação assíncrona leva o mesmo tempo para ser processada como uma solicitação síncrona. Por exemplo, 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 se ela for 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 síncronos ou assíncronos

Esta seção lista as diretrizes para quando usar métodos 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 extensa sobrecarga de disco ou rede. O uso de métodos assíncronos 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 bloquear o thread de solicitação ASP.NET enquanto não faz nenhum trabalho. Ao tornar a chamada assíncrona, o thread de solicitação ASP.NET não é bloqueado enquanto aguarda a conclusão da solicitação do serviço Web.

  • Os testes mostram 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 assíncronos com eficiência. O exemplo fornecido foi projetado para fornecer uma demonstração simples de programação assíncrona no ASP.NET 4.5. O exemplo não se destina a ser uma arquitetura de referência para programação assíncrona em ASP.NET. 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 para usar métodos assíncronos.

Poucos aplicativos exigem que todos os métodos sejam assíncronos. Muitas vezes, converter alguns métodos síncronos 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 no https://github.com/RickAndMSFT/Async-ASP.NET site do GitHub . O repositório consiste em três projetos:

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

A Página Síncrona do Gizmos

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

public partial class Gizmos : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var gizmoService = new GizmoService();
        GizmoGridView.DataSource = gizmoService.GetGizmos();
        GizmoGridView.DataBind();
    }
}

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 aparelhos. O projeto WebAPIpgw contém a implementação da API gizmos, widget Web e product dos controladores.
A imagem a seguir mostra a página de aparelhos do projeto de exemplo.

Captura de tela da página do navegador da Web Sync Gizmos mostrando a tabela de aparelhos com os detalhes correspondentes, conforme inserido nos controladores de API Web.

Criando uma página gizmos assíncrona

O exemplo usa as novas palavras-chave assíncronas 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.

ASP.NET páginas assíncronas devem incluir a diretiva Page com o Async atributo definido como "true". O código a seguir mostra a diretiva Page com o Async atributo definido como "true" para a página GizmosAsync.aspx .

<%@ Page Async="true"  Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosAsync.aspx.cs" Inherits="WebAppAsync.GizmosAsync" %>

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

protected void Page_Load(object sender, EventArgs e)
{
   var gizmoService = new GizmoService();
   GizmoGridView.DataSource = gizmoService.GetGizmos();
   GizmoGridView.DataBind();
}

A versão assíncrona:

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcAsync));
}

private async Task GetGizmosSvcAsync()
{
    var gizmoService = new GizmoService();
    GizmosGridView.DataSource = await gizmoService.GetGizmosAsync();
    GizmosGridView.DataBind();
}

As alterações a seguir foram aplicadas para permitir que a GizmosAsync página seja assíncrona.

  • A diretiva Page deve ter o Async atributo definido como "true".
  • O RegisterAsyncTask método é usado para registrar uma tarefa assíncrona que contém o código que é executado de forma assíncrona.
  • O novo GetGizmosSvcAsync 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 que é retornado.
  • "Async" foi acrescentado ao nome do método assíncrono. Acrescentar "Async" não é necessário, mas é a convenção ao escrever métodos assíncronos.
  • O tipo de retorno do novo GetGizmosSvcAsync método é Task. O tipo de retorno de Task 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.
  • O palavra-chave de espera foi aplicado à chamada do serviço Web.
  • A API de serviço Web assíncrona foi chamada (GetGizmosAsync).

Dentro do corpo do GetGizmosSvcAsync 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 de espera 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 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 (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            await webClient.DownloadStringTaskAsync(uri)
        );
    }
}

As alterações assíncronas são semelhantes às feitas ao 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 síncrona.
  • O palavra-chave de espera foi aplicado ao método assíncrono HttpClientGetAsync.

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

Captura de tela da página do navegador da Web Gizmos Async mostrando a tabela de aparelhos com detalhes correspondentes, conforme inserido nos controladores de API Web.

A apresentação de navegadores dos dados de aparelhos é 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.

Notas de RegisterAsyncTask

Os métodos conectados com RegisterAsyncTask serão executados imediatamente após o PreRender.

Se você usar eventos de página assíncrona nulos diretamente, conforme mostrado no seguinte código:

protected async void Page_Load(object sender, EventArgs e) {
    await ...;
    // do work
}

você não tem mais controle total sobre quando os eventos são executados. Por exemplo, se um .aspx e um . Os eventos de definição Page_Load mestre e um ou ambos são assíncronos, a ordem de execução não pode ser garantida. A mesma ordem indeterminada para manipuladores de eventos (como async void Button_Click ) se aplica.

Executando várias operações em paralelo

Métodos assíncronos têm uma vantagem significativa sobre métodos síncronos quando uma ação deve executar várias operações independentes. No exemplo fornecido, a página síncrona PWG.aspx(for Products, Widgets e Gizmos) exibe os resultados de três chamadas de serviço Web para obter uma lista de produtos, widgets e aparelhos. 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, a página assíncrona PWGasync.aspx leva pouco mais de 500 milissegundos para ser concluída enquanto a versão síncrona PWG leva mais de 1.500 milissegundos. A página PWG.aspx síncrona é mostrada no código a seguir.

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();

    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );
    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();

    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}", 
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

O código assíncrono PWGasync atrás é mostrado abaixo.

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();
    RegisterAsyncTask(new PageAsyncTask(GetPWGsrvAsync));
    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}",
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

private async Task GetPWGsrvAsync()
{
    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
       );

    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();           
}

A imagem a seguir mostra a exibição retornada da página assíncrona PWGasync.aspx .

Captura de tela da página do navegador da Web Widgets, Produtos e Gizmos assíncronas mostrando as tabelas Widgets, Produtos e Gizmos.

Usando um token de cancelamento

Os métodos assíncronos retornados Tasksão canceláveis, ou seja, eles tomam um parâmetro CancellationToken quando um é fornecido com o AsyncTimeout atributo da diretiva Page . O código a seguir mostra a página GizmosCancelAsync.aspx com um tempo limite de em segundo.

<%@ Page  Async="true"  AsyncTimeout="1" 
    Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosCancelAsync.aspx.cs" 
    Inherits="WebAppAsync.GizmosCancelAsync" %>

O código a seguir mostra o arquivo GizmosCancelAsync.aspx.cs .

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcCancelAsync));
}

private async Task GetGizmosSvcCancelAsync(CancellationToken cancellationToken)
{
    var gizmoService = new GizmoService();
    var gizmoList = await gizmoService.GetGizmosAsync(cancellationToken);
    GizmosGridView.DataSource = gizmoList;
    GizmosGridView.DataBind();
}
private void Page_Error(object sender, EventArgs e)
{
    Exception exc = Server.GetLastError();

    if (exc is TimeoutException)
    {
        // Pass the error on to the Timeout Error page
        Server.Transfer("TimeoutErrorPage.aspx", true);
    }
}

No aplicativo de exemplo fornecido, selecionar o link GizmosCancelAsync chama a página GizmosCancelAsync.aspx e demonstra o cancelamento (por tempo limite) da chamada assíncrona. Como o tempo de atraso está dentro de um intervalo aleatório, talvez seja necessário atualizar a página algumas vezes para obter a mensagem de erro de tempo limite.

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 do servidor padrão. Tenha em mente o seguinte ao configurar e testar o estresse de seu aplicativo Web assíncrono.

  • Windows 7, Windows Vista, Janela 8 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 de um prompt de comando com privilégios elevados usando o seguinte comando:
    %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 filaHTTP.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 de 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.
      Captura de tela do Gerenciador de Serviços de Informações da Internet mostrando o menu Configurações Avançadas realçado com um retângulo vermelho.
    • Na caixa de diálogo Configurações Avançadas , altere o Comprimento da Fila de 1.000 para 5.000.
      Captura de tela da caixa de diálogo Configurações Avançadas mostrando o campo Comprimento da Fila definido como 1000 e realçado com um retângulo vermelho.

    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:

  • Controle de versão do .NET e Multi-Targeting – o .NET 4.5 é uma atualização in-loco para o .NET 4.0

  • Como definir um Aplicativo IIS ou AppPool para usar ASP.NET 3.5 em vez de 2.0

  • Versões e dependências do .NET Framework

  • 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 de configuração automática 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 à configuração automática, 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.

Colaboradores