// Rodolfo Fadino

/* LIFE RUNS ON CODE */

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

ASP.NET