in ASP.NET

Controllers Assíncronos [ AsyncController] com ASP.NET MVC

Existem diversos cenários em que uma requisição pode demorar para ser processada, por exemplo em chamadas que aguardam algum retorno de um webservice, ou em chamadas que exigem consultas demoradas a base de dados ou em aplicações real-time, que ficam aguardando alguma mudança para retornar a requisição (ex: chat).

No IIS, o .NET mantém um Pool de Threads (numero limitado de Threads) que são utilizadas para processar as requisições que chegam. Quando uma requisição chega, uma Thread deste Pool é utilizada para processar a requisição. Se esta requisição é processado de uma maneira síncrona, a Thread que processa a requisição é bloqueada enquanto a solicitação é processada, e essa Thread não pode atender a outra requisição durante esse período.

Em cenários de baixa concorrência, ou de requisições simples isso pode não ser um problema, porque o pool de threads pode ser grande o suficiente para acomodar muitas requisições bloqueados. Entretanto, como o número de threads no pool é limitado, em condições de muitos acessos, grande concorrência, e ou requisições demoras, acontece um enfileiramento destas requisições. Quando essa fila se torna muito grande, o servidor Web começa a rejeitar pedidos com um status HTTP 503 (Server Too Busy).

Processando Requisições Assíncronas

Em situações onde este enfileiramento pode acontecer é possível tratar de maneira assíncrona as requisições que levam mais tempo para serem processadas. Lembrando que uma requisição assíncrona demora a mesma quantidade de tempo para processar do que uma requisição tratada de maneira síncrona. Exemplo, se uma requisição faz uma chamada para um webservice e demora 1 segundo pata completar, a requisição leva 1 segundo se ele for realizada de forma síncrona ou assíncrona. Porem, durante uma requisição assíncrona, aquela Thread do Pool não é bloqueada para responder a outros pedidos enquanto aguarda o primeiro pedido para ser concluído.

Portanto, solicitações assíncronas evitam filas quando há muitas requisições que possuem um longo tempo de duração.

Como Acontece?

Quando uma ação assíncrona é invocada, ocorrem as seguintes etapas:

O IIS recebe uma Thread do Pool de Threads (worker thread pool) e designa ela para lidar com uma solicitação de entrada. Esta work thread inicia a operação assíncrona.

A worker thread é devolvida ao pool de threads para atender outra requisição Web.

Quando a operação assíncrona for concluída, ele notifica ASP.NET.

O IIS recebe uma worker thread do pool de threads (que pode ser um thread diferente do thread que iniciou a operação assíncrona) para processar o restante do pedido, incluindo o processamento da resposta.

 

Como escolher entre Controllers Síncronos e Assíncronos?

Cada cenário possui diversas características unicas que influenciam na escolha entre a abordagem síncrona e a assíncrona, entretanto podemos desenhar algumas características para criar as principais recomendações:

Síncrono: use a abordagem sincrona quando:

  • as operações são simples e de curta duração;
  • simplicidade é mais importante do que eficiência;
  • as operações são basicamente de CPU ao invés de utilizarem muito disco ou rede. métodos assincronos em operações do tipo CPU-bound não oferecem resultados melhore
Assíncrono: use a abordagem assincrona quando:
  • as operações são rede-bound ou I / O-bound em vez de CPU-bound (leitura de arquivos, chamadas de webservices, etc);
  • teste mostram que as operações de bloqueio são um gargalo no desempenho do site e que o IIS pode atender mais solicitações, utilizando métodos de ação assíncrona para estas chamadas de bloqueio;
  • paralelismo é mais importante do que a simplicidade do código;
  • você quer fornecer um mecanismo que permite aos usuários cancelar um pedido de longa duração;
Como implementar Controllers Assíncronos no ASP.NET MVC
Como exemplo, vou criar uma aplicação em MVC3

Após isso vou escolher iniciar com um exemplo de projeto ( Internet Application)

Isto nos cria a seguinte estrutura de projetos

Se analisarmos o Controller principal do projeto ( HomeController) veremos o seguinte codigo

public class HomeController : Controller
{
    public ActionResult Index()
    {
        ViewBag.Message = "Welcome to ASP.NET MVC!";

        return View();
    }

    public ActionResult About()
    {
        return View();
    }
}

a classe HomeController herda da classe abstrata Controller

public class HomeController : Controller

Para trabalharmos com um Controller Assíncrono, a primeira coisa que devemos fazer é com que a classe do Controller herde da classe abstrata AsyncController

public class HomeController : AsyncController

Após isso precisamos mudar a forma de abordar as Actions da classe, desacoplando o inicio dela em um método que retornará void e outro método que executará o processamento e retornará o esperado pelo cliente, alem disso devemos utilizar o padrão dos nomes dos métodos, no primeiro adicionaremos o final Async e no retorno o Completed

public class HomeController : AsyncController
{
    public void IndexAsync()
    {
        //Inicio da requisição
    }

    public ActionResult IndexCompleted()
    {
        //Retorno
        return View();
    }
}

O ASP.NET controla a requisição assincrona com o AsyncManager, por isso precisamos utilizar o padrão nos nomes dos métodos, o AsyncManager funciona resumidamente como um gerenciador do estado da requisição, quando o contador é zerado ele aciona o método de retorno.

Criei uma classe chamada Processamento, que tem um método chamado Processar, que demora 10000 milisegundos para retornar.

public class Processamento
{
    public void Processar()
    {
        Thread.Sleep(10000);
        return;
    }
}

Agora no método IndexAsync eu farei 3 coisas, primeiro adicionarei um item ao contador no AsyncManager, após isso criarei meu objeto Processamento, e por último utilizando a classe Task (http://msdn.microsoft.com/en-us/library/dd235678.aspx) do .NET chamarei o método Processar de uma maneira assíncrona, nessa operação assíncrona, eu não posso esquecer de retirar um item do contador do AsyncManager quando o Processar terminar.

public void IndexAsync()
{
    AsyncManager.OutstandingOperations.Increment();

    var processamento = new Processamento();

    Task.Factory.StartNew(() =>
    {
        processamento.Processar();
        AsyncManager.OutstandingOperations.Decrement();
    });
}

Assim que retirarmos um Item do AsyncManager, ele retornará a requisição chamando o IndexCompleted

public ActionResult IndexCompleted()
{
    return View();
}

Abaixo podemos ver como ficaram as classes que utilizei neste exemplo

public class HomeController : AsyncController
{
    public void IndexAsync()
    {
        AsyncManager.OutstandingOperations.Increment();

        var processamento = new Processamento();

        Task.Factory.StartNew(() =>
        {
            processamento.Processar();
            AsyncManager.OutstandingOperations.Decrement();
        });
    }

    public ActionResult IndexCompleted()
    {
        return View();
    }

}
public class Processamento
{
    public void Processar()
    {
        Thread.Sleep(10000);
        return;
    }
}

Exitem diversas maneira e funcionalidades que podemos explorar com o AsyncController, como por exemplo a passagem de parâmetros do método assíncrono para nosso método de retorno da requisição, para isso vou alterar meu método Processar para ele retornar um int

public class Processamento
{
    public int Processar()
    {
        Thread.Sleep(10000);
        return 2;
    }
}

Agora vou utilizar o AsyncManager para passar o valor

Task.Factory.StartNew(() =>
{
    var retorno =processamento.Processar();
    AsyncManager.Parameters["quantidade"] = retorno;
    AsyncManager.OutstandingOperations.Decrement();
});

Finalmente preciso alterar meu método de retorno o IndexCompleted para ele receber o parâmetro, aproveitei e atribui ele a um ViewBag chamado Quantidade, para exibir na View.

public ActionResult IndexCompleted(int quantidade)
{
    ViewBag.Quantidade = quantidade;
    return View();
}

Bom, acho que o mais importante é sabermos identificar em nossos cenários qual abordagem utilizar, sendo que se precisarmos de uma abordagem assíncrona o ASP.NET MVC oferece um excelente suporte.

muito obrigado

Rodolfo

  • http://twitter.com/cleberdantas Cleber Dantas

    De nada

  • Rodrigo Pires

    Artigo sensacional!

  • http://www.rodolfojunior.com/2011/09/usando-long-polling-com-asynccontrollers/ Usando Long Polling com AsyncControllers | // Rodolfo Junior

    […] excelentes recursos para implementarmos o Long Polling, um deles é o oferecido pelo ASP.NET MVC, o AsyncController. Com ele é possivel criarmos requisições que podem ficar aguardando uma resposta nova, sem que […]

  • http://www.dotnetflash.com.br/ Marcell Nascimento Alves

    Esse artigo me ajudou muito no entendimento do funcionamento de chamadas assíncronas no .Net. Obrigado!

  • Daniel Vieira Costa

    Bacana este artigo, no entanto, seria interessante atualizar com base em async Task sendo que AsyncController é uma classe vazia.

  • http://www.rodolfofadino.com.br/ Rodolfo Fadino

    Sim Daniel, faz muito tempo que escrevi este post, nas versoes mais novas do ASP.NET MVC, é só mudar o retorno da action, não tinha pensado em escrever novamente, vou seguir a dica 🙂

  • Eduardo Henrique Rizo

    Great !