Páginas Assíncronas

No momento em que o ASP.NET 1.x recebe uma requisição para uma determinada página, o runtime do ASP.NET resgata uma Thread do Pool e atribui esta à requisição para que a mesma seja executada. Isso faz com que todo o processo, desde a inicialização até a renderização, seja processado por essa Thread. Isso é conhecido como um processo (ou página) síncrono, ou seja, a Thread que está sendo utilizada somente será liberada no momento em que esse processo finalizar.

Neste caso, por processo eu me refiro à uma consulta a uma base de dados ou até mesmo uma chamada a um Web Service. Se a Thread está bloqueada/ocupada processando uma operação, quando novas requisições chegarem elas vão sendo enfileiradas, onde aguardam a liberação da Thread para que em seguida elas sejam processadas. Isso é problemático, pois se a fila “encher”, será retornado ao usuário o erro do tipo 503 (“Server Unavailable”). A imagem abaixo ilustra o processo de requisição em uma aplicação ASP.NET 1.x:

Figura 1 – Requisição de uma página no ASP.NET 1.x.

Como vemos, depois do processo finalizado, a Thread é devolvida para o Pool. O ASP.NET 1.x não fornece um suporte para páginas assíncronas, mas isso não é impossível. O segredo está em implementar a interface IHttpAsyncHandler, mas isso já foge do escopo deste artigo. Ao contrário do ASP.NET 1.x, o ASP.NET 2.0 já traz instrinsicamente uma infra-estrutura para trabalharmos com páginas/chamadas assíncronas, simplificando bastante a sua utilização. A imagem abaixo ilustra como uma página ASP.NET 2.0 assíncrona trabalha:

Figura 2 – Requisição de uma página no ASP.NET 2.0.

Os detalhes para a utilização das páginas assíncronas começam efetuando uma configuração na página ASPX, ou seja, devemos definir na diretiva de página um novo atributo chamado Async, que recebe um valor booleano (Verdadeiro ou Falso) indicando se a página irá ter ou não um processamento assíncrono. O trecho de código abaixo exibe essa configuração no arquivo ASPX:

<%@ Page Async="True" ... %>

Depois desta configuração, que é extremamente necessária para esse tipo de processamento, devemos chamar o método AddOnPreRenderCompleteAsync da classe Page no evento Load do WebForm. Esta função receberá dois handlers/delegates: o procedimento que o ASP.NET executará assincronamente (BeginEventHandler) e o procedimento que será executado quando o mesmo for finalizado (EndEventHandler). Como já dissemos anteriormente, podemos fazer aqui uma chamada à um Web Service ou efetuar uma query a uma base de dados e, conseqüentemente, exibir os dados para o usuário. Veremos abaixo como configurar o método AddOnPreRenderCompleteAsync no evento Load do WebForm, onde vamos passar os procedimentos que deverão ser executados:

Page.AddOnPreRenderCompleteAsync(
    new BeginEventHandler(IniciaProcesso),
    new EndEventHandler(FinalizaProcesso)
);

Quando a requisição à esta página acontece, o ASP.NET resgata a Thread do Pool e executa os passos (eventos) até o evento PreRender do WebForm e devolve esta Thread ao Pool. O procedimento que é disparado assincronamente deve retornar um IAsyncResult, que irá determinar se a operação foi completada. Neste momento, uma nova Thread é recuperada do Pool e executa o segundo procedimento (o qual também passamos para o método AddOnPreRenderCompleteAsync (“FinalizaProcesso”)). Depois que esse procedimento finalizar, essa mesma Thread executa o restante dos eventos que compõem o ciclo de vida de uma página ASP.NET até o seu retorno ao cliente, ou seja, quando o Response é criado. Isso explica detalhadamente o que vimos na Figura 2. Mas a desvantagem neste caso é que dentro do procedimento “FinalizaProcesso” não temos acesso ao contexto da requisição (HttpContext.Current), porém temos formas de conseguir contornar isso, que veremos em um futuro artigo.

Este tipo de implementação não prende as requisições subseqüentes até que o processo seja finalizado, ou seja, quando a parte mais complexa/custosa de uma página começar a ser executada, já devolvemos a Thread ao Pool para que a mesma possa servir as próximas requisições enquanto o processo assíncrono acontece. A Figura 3 nos ajuda a comparar os eventos de uma página síncrona versus uma página assíncrona para ver e perceber o momento exato em que as coisas acontecem, podendo ser confirmado através do Trace de uma determinada página assíncrona, que é mostrado na Figura 4, logo em seguida.

Figura 3 – Ciclo de Vida – Síncrona vs. Assíncrona.

 

Figura 4 – Trace de uma página Assíncrona.

Acesso à Base de Dados

É bastante comum em páginas ASP.NET acessarmos o conteúdo de uma determinada base de dados; sendo assim, podemos efetuar uma query assíncrona dentro de uma base de dados qualquer e retornar ao usuário o result-set e, conseqüentemente, popular um controle do tipo GridView. Devemos, neste caso, criar e codificar o evento PreRenderComplete, que será disparado imediatamente depois do processo assíncrono finalizado, mas antes da página ser renderizada, justamente para termos acesso ao controle GridView e aos demais itens da página.

A execução assíncrona de queries na base de dados é possível devido às novas funcionalidades que temos dentro do ADO.NET 2.0, ou seja, o objeto SqlCommand ganhou vários novos métodos para executar trabalho assíncrono. Entre esses novos métodos temos: BeginExecuteReader e EndExecuteReader. Além disso, a ConnectionString também deverá acrescer um novo parâmetro ([…];Asynchronous Processing=true) para indicar que a mesma poderá trabalhar de forma assíncrona. Isso irá fazer com que a nossa aplicação se torne muito mais escalável, pois como já vimos anteriormente, o processo não prenderá a Thread até que a query seja retornada. Veremos abaixo o código necessário para a execução de uma query assíncrona dentro de uma página também assíncrona:

using System.Data.SqlClient;

public partial class DB : System.Web.UI.Page
{
    private const string CONN_STRING = @"[...];Asynchronous Processing=true";
    private SqlConnection _conn;
    private SqlCommand _cmd;
    private SqlDataReader _reader;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            this.PreRenderComplete += new EventHandler(Page_PreRenderComplete);

            this.AddOnPreRenderCompleteAsync(
                new BeginEventHandler(IniciaProcesso),
                new EndEventHandler(FinalizaProcesso)
            );
        }
    }

    protected IAsyncResult IniciaProcesso(
        object sender, 
        EventArgs e, 
        AsyncCallback cb, 
        object state)
    {
        this._conn = new SqlConnection(CONN_STRING);
        this._conn.Open();
        this._cmd = new SqlCommand("SELECT * FROM Usuarios", this._conn);
        return this._cmd.BeginExecuteReader(cb, state);
    }

    protected void FinalizaProcesso(IAsyncResult ar)
    {
        this._reader = this._cmd.EndExecuteReader(ar);
    }

    protected void Page_PreRenderComplete(object sender, EventArgs e)
    {
        if (this._reader.HasRows){
            this.GridView1.DataSource = _reader;
            this.GridView1.DataBind();		
        }
    }

    public override void Dispose()
    {
        if (this._conn != null) this._conn.Close();
        base.Dispose();
    }
}

Analisando o código acima, vemos que no evento Load do WebForm definimos o método que será executado assincronamente e também o método que será executado quando o processo for finalizado. Quando o processo se inicia, o código do procedimento “IniciaProcesso” é executado, ou seja, fazemos a consulta na base de dados e, quando finalizado, o procedimento “FinalizaProcesso” é executado e nele atribuimos o retorno do método EndExecuteReader, que irá retornar um objeto do tipo SqlDataReader para o membro _reader da página.

Se analisarmos novamente a Figura 4, vemos que o evento PreRenderComplete é disparado depois que o procedimento assíncrono é finalizado, e é neste momento que devemos atribuir o resultado ao nosso controle GridView, pois aqui a página não foi ainda renderizada. Finalmente sobrescrevemos o método Dispose da classe base (Page), onde fazemos o fechamento da conexão com a base de dados. Não esquecer em hipótese alguma de chamar também o método Dispose da classe base.

Vale lembrar também que o método BeginExecuteReader tem um overload (sobrecarga) que pode receber como parâmetro um enumerador do tipo CommandBehavior, onde podemos definí-lo como CloseConnection e, quando fecharmos a conexão com o DataReader através do seu método Close, a conexão com a base de dados é automaticamente fechada. Quando definimos o CommandBehavior como CommandBehavior, o DataReader já será fechado automaticamente depois do método DataBind ser executado e, conseqüentemente, fechando a conexão com a base de dados que está vinculada a ele. Faz sentido ele fechar logo após o método DataBind pois, como sabemos, o DataReader somente avança, não faz mais sentido termos ele “ativo”, já que o GridView o utilizou.

Acesso à Web Services

Os WebServices já fornecem por padrão métodos para serem executados assincronamente, como já deve ser conhecido de todos. Para certificar-se disso, experimente criar um método qualquer em seu arquivo ASMX marcado com o atributo WebMetod e verá que, quando fizer uma Web Reference em seu projeto verá que, além do método criado, também serão disponibilizados mais dois métodos, como por exemplo: se definiu o método chamado “RecuperaUsuarios” então terá também “BeginRecuperaUsuarios” e “EndRecuperaUsuarios”. Veremos abaixo como invocar esses WebServices assincronamente:

using System.Data;

public partial class WS : System.Web.UI.Page
{
    private DataSet _ds;
    private UsuariosWS.Usuarios _wsUsuarios;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            this.PreRenderComplete += new EventHandler(Page_PreRenderComplete);

            this.AddOnPreRenderCompleteAsync(
                new BeginEventHandler(IniciaProcesso),
                new EndEventHandler(FinalizaProcesso)
            );
        }
    }

    protected IAsyncResult IniciaProcesso(
        object sender,
        EventArgs e,
        AsyncCallback cb,
        object state)
    {
        this._wsUsuarios = new UsuariosWS.Usuarios();
        this._wsUsuarios.UseDefaultCredentials = true;
        return this._wsUsuarios.BeginRecuperaUsuarios(cb, state);
    }

    protected void FinalizaProcesso(IAsyncResult ar)
    {
        this._ds = this._wsUsuarios.EndRecuperaUsuarios(ar);
    }

    protected void Page_PreRenderComplete(object sender, EventArgs e)
    {
        this.GridView1.DataSource = this._ds.Tables[0];
        this.GridView1.DataBind();
    }

    public override void Dispose()
    {
        if (this._wsUsuarios != null) this._wsUsuarios.Dispose();
        base.Dispose();
    }
}

Se analisarmos o código acima, veremos que o mecanismo é bem parecido ao que utilizamos para efetuar o acesso/query à base de dados utilizando o método AddOnPreRenderCompleteAsync da classe Page para definir o processo assíncrono. A única diferença aqui é que o retorno, que recuperamos através do método EndRecuperaUsuarios, é atribuído ao DataSet, que é um membro da página. Este por sua vez, é definido como fonte de dados para o controle GridView.

Além dessas novas funcionalidades, temos ainda algumas outras que não abordei aqui, como por exemplo o método [Metodo]Async e o evento [Metodo]Completed, os quais são encontrados nos proxies dos Web Services do .NET Framework 2.0. Temos também as chamadas Asynchronous Tasks, ou seja, tarefas assíncronas. Ambos fornecem algumas vantagens em relação ao que vimos no decorrer deste artigo, onde deixarei para explicá-las em um futuro e próximo artigo.

CONCLUSÃO: Vimos neste artigo a nova forma em que as páginas ASP.NET 2.0 trabalham e como as mesmas são processadas. Conseguimos através das figuras, que comparam a versão 1.x versus a versão 2.0, perceber o ganho e a flexibilidade que agora são fornecidos internamente pelo ASP.NET 2.0, sem a necessidade de escrevermos muito código para alcançar este objetivo.

AsyncPages.zip (14.95 kb)

Parâmetros para Thread

Fiquei curioso para saber como podemos passar parametros para Threads, ou seja, quando definimos um método de uma determinada classe que nossa Thread vai executar quando a mesma for iniciada. Atualmente não se tem muitos recursos para fazer isso e para conseguirmos suprir essa necessidade definimos o construtor da classe para receber o parametro que desejamos passar para que o método o utilize. Em código, fica mais ou menos como:

     class MinhaApp
     {
          [STAThread]
          static void Main(string[] args)
          {
               Teste t = new Teste(“Meu Parametro”);
               Thread tr = new Thread(new ThreadStart(t.Escreve));
               tr.Start();
          }
     }

     public class Teste{
          private string _param;
          public Teste(string param){
               this._param = param;
          }
          public void Escreve(){
               Console.WriteLine(this._param);
          }
     }

Como podemos ver, a classe “Teste” recebe em seu construtor o(s) parametro(s) que vamos utilizar dentro do método “Escreve” e, quando o método for executado, teremos a certeza que os parametros estarão disponíveis para a utilização.

Andei analisando e, na versão 2.0 do .NET Framework temos um novo delegate do tipo ParameterizedThreadStart, que recebe um parametro do tipo Object e, para o ideal funcionamento deste, o método Start da classe Thread agora tem um overload, que recebe o parametro do tipo Object que será passado para o delegate. Em conjunto com os métodos anonimos, o código fica bastante elegante:

     class Program
     {
          static void Main(string[] args)
          {
               ParameterizedThreadStart pts = new ParameterizedThreadStart(delegate(object o)
               {
                    Console.WriteLine(o.ToString());
               });
               
               Thread t = new Thread(pts);
               t.Start(“Meu Parametro”);
          }
     }

System.ComponentModel.BackgroundWorker

Dando sequencia as novas “features” do .NET 2.0 (já que só falam disso :P), eis aqui uma nova classe para trabalharmos em nossas aplicações. Como o próprio nome da classe diz, BackgroundWorker, você pode criar uma nova tarefa para este executar em modo assíncrono, e com isso uma nova Thread é criada para a execução deste trabalho.

Esta classe é composta por dois principais eventos: DoWork e RunWorkerCompleted. O evento DoWork é disparado quando é iniciado o trabalho que este terá que fazer. Já o RunWorkerCompleted, é disparado quando o processo assíncrono, que está sendo executado, é finalizado.

Para iniciarmos o processo, chamamos o método RunWorkerAsync do objeto BackgroundWorker, que tem um overload, onde você pode passar algum parâmetro do tipo Object para a rotina. Para ilustrarmos o processo, vamos carregar assincronamente um DataGridView com os dados vindos de uma DB, neste caso, Northwind. O código para isso é:

     Imports System.Data
     Imports System.Data.SqlClient

     Public Class Form1

         Private WithEvents worker As New System.ComponentModel.BackgroundWorker
         Private table As DataTable

         Private Sub Button1_Click( _
             ByVal sender As System.Object, _
             ByVal e As System.EventArgs) _
             Handles Button1.Click

             worker.RunWorkerAsync()
         End Sub

         Private Sub worker_DoWork( _
             ByVal sender As Object, _
             ByVal e As System.ComponentModel.DoWorkEventArgs) _
             Handles worker.DoWork

             Using conn As New SqlConnection(“Server=localhost;Integrated Security=True;Database=Northwind”)
                 Dim adapter As New SqlDataAdapter(New SqlCommand(“SELECT * FROM Customers”, conn))
                 table = New DataTable
                 adapter.Fill(table)
             End Using
         End Sub

         Private Sub worker_RunWorkerCompleted( _
             ByVal sender As Object, _
             ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
             Handles worker.RunWorkerCompleted

             LoadData()
         End Sub

         Private Sub LoadData()
             Me.DataGridView1.DataSource = table
         End Sub

     End Class

Como podemos ver, no evento Click do botão chamamos o método RunWorkerAsync, que dispara/processa o código do evento DoWork. No exemplo acima, resgatamos os dados da tabela Customers do Banco de Dados Northwind assincronamente e quando finalizado, o evento RunWorkerCompleted é invocado e assim chamamos a rotina que define o DataTable como DataSource do controle DataGridView.

Nota: O que se deve ter atenção é que você não pode atualizar os controles de UI dentro deste processo porque eles (controles de UI) podem ser atualizados apenas pela Thread que os criaram – a Thread que chamou a UI – exatamente por isso que a definição do DataSource (que atualiza o controle) é desvinculada do evento de carga do DataTable.