Gerardo Contijoch

Experiencias del día a día trabajando con .NET – ASP.NET, C#, ASP.NET MVC y demas…

Ejecutar tareas de forma asincrónica en ASP.NET 2.0 (Parte 1)

Posted by Gerardo Contijoch en enero 20, 2009

Navegando por los links de uno de los posts que me encontré hace un tiempo en mi feed reader, encontré un artículo interesantísimo de Jeff Prosise sobre ejecución asincrónica de páginas en ASP.NET 2.0 que no quería dejar de comentar. Ya hace tiempo que trabajo sobre el Framework 2.0, pero por algún motivo nunca llegué a leer algo sobre el tema (¡era hora que lo hiciera!). El artículo del que hablo es éste, y como no encontré mucho material en español sobre el tema voy a comentarles un poco de que trata el artículo y como lograr que las páginas en ASP.NET 2.0 se procesen (al menos en parte) de manera asincrónica.

Cabe aclarar que con las técnicas que voy a presentar nuestros sites no necesariamente se van a cargar más rápido de una manera perceptible para el usuario, sino que más bien se van a aprovechar los recursos para evitar tiempos muertos durante el procesamiento de las páginas.

Es necesario tener conocimiento sobre el ciclo de vida de las paginas en ASP.NET para poder entender como se logra la asincronicidad, por lo que voy a describir esto brevemente.

Ciclo de vida de una página en ASP.NET

Toda página tiene un ciclo de vida que comienza luego de que desde un cliente (como puede ser un browser) se hace una petición o Request por una página. A lo largo de este ciclo de vida se producen distintos eventos que nos van notificando por las etapas por las cuales esta pasando una página. Por ejemplo, el evento Init se produce luego de cargar los controles y es el momento de inicializarlos (setear sus propiedades). Por otro lado, el evento PreRender se da un instante antes de comenzar el renderizado de la página por lo que puede ser un buen lugar para modificar algún detalle de último momento relacionado con la visualización o renderizado de la página (yo tengo la costumbre de hacer visibles/invisibles o habilitar/deshabilitar ciertos controles en este punto y no antes, de modo que la lógica de presentación no se mezcle con la relacionada al binding de grillas, por ejemplo). Pueden encontrar el detalle del ciclo de vida de una página acá (a los más curiosos les recomiendo investigar el método ProcessRequestMain(bool, bool) de la clase Page con .Net Reflector).

El ciclo completo de una página es el siguiente:

  • PreInit
  • Init
  • InitComplete
  • LoadState (evento privado)
  • ProcessPostData (evento privado)
  • PreLoad
  • Load
  • ProcessPostData (una segunda vez)
  • “ChangedEvents” (eventos lanzados por los controles de la página)
  • PostBackEvents (evento privado)
  • LoadComplete
  • PreRender
  • PreRenderComplete
  • SaveState
  • SaveStateComplete
  • Unload

Cuando una página implementa la interface IHttpHandler, el ciclo de vida completo es ejecutado por un único hilo recuperado del ThreadPool, el cual es devuelto una vez finalizada la ejecución de la página. La mayoría de las paginas muestran datos que son procesados o recuperados de algún lado, ya sea una DB, un archivo de configuración, etc, y esa tarea casi siempre es independiente del procesamiento de la página. Si tardamos 5 segundos en recuperar los datos de una tabla de una DB, esos son 5 segundos de tiempo muerto en el que el hilo que esta ejecutando el procesamiento de la página se queda esperando los datos. Como el ciclo de vida de una página es necesariamente secuencial, no podemos aprovechar este tiempo muerto para procesar otras partes de la página, pero lo que si podemos hacer es liberar el hilo (es decir, devolverlo al ThreadPool) de modo que pueda ser reutilizado para el procesamiento de alguna otra tarea.

Ejecución asincrónica

Veamos como hacerlo. Lo primero es habilitar la ejecución asincrónica seteando el atributo Async en la directiva Page a true:

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

Lo que esto produce es que la página implemente la interface IHttpAsyncHandler (a diferencia de las páginas ‘comunes’, que implementan IHttpHandler). No entiendo muy bien en dónde ni cómo ocurre esto ya que la clase Page implementa IHttpHandler y no se pueden cambiar sus interfaces así como así. Evidentemente todo este proceso ocurre antes de la propia compilación de la página (ya que luego de la misma, esta no puede ser modificada), pero no se exactamente en que punto (si alguien sabe, que diga).

En fin, me estoy llendo por las ramas. Al implementar esta interface, vamos a poder introducir dos eventos nuevos en el ciclo de vida de la página. Estos se van a ejecutar inmediatamente después del evento PreRender y antes de PreRenderComplete . El primero de ellos lo que va a hacer es ejecutar una tarea de manera asincrónica devolviendo un objeto que implemente IAsyncResult. El segundo, va a ser lanzado cuando el proceso asincrónico finalice, retomando ahí lo que queda del procesamiento de la página.

Para atrapar estos eventos vamos a hacer uso del método AddOnPreRenderCompleteAsync de la clase Page:

   1: public partial class AsyncForm : System.Web.UI.Page {
   2:     protected void Page_Load(object sender, EventArgs e) {
   3:         Page.AddOnPreRenderCompleteAsync(
   4:             new BeginEventHandler(this.BeginAsync),
   5:             new EndEventHandler(this.EndAsync)
   6:         );
   7:     }
   8:
   9:     IAsyncResult BeginAsync(object sender, EventArgs e, AsyncCallback cb, object state) {
  10:         /*...*/
  11:     }
  12:
  13:     void EndAsync(IAsyncResult ar) {
  14:         /*...*/
  15:     }
  16: }

Debido a que los eventos estos son lanzados antes del evento PreRenderComplete, es necesario que este método sea ejecutado antes de llegar a esta fase del procesamiento de la página.

Veamos ahora como recuperar datos para cargar una grilla de manera asincrónica:

   1: public partial class AsyncForm : System.Web.UI.Page {
   2:
   3:     SqlCommand cmd;
   4:
   5:     protected void Page_Load(object sender, EventArgs e) {
   6:        Page.AddOnPreRenderCompleteAsync(
   7:            new BeginEventHandler(this.RecuperarDatos),
   8:            new EndEventHandler(this.CargarGrilla)
   9:        );
  10:     }
  11:
  12:     IAsyncResult RecuperarDatos(object sender, EventArgs e, AsyncCallback cb, object state) {
  13:        SqlConnection cnn = new SqlConnection("...");
  14:        cnn.Open();
  15:
  16:        this.cmd = new SqlCommand("SELECT * FROM ...", cnn);
  17:
  18:        // Atencion con el CommandBehavior, si no lo usamos de esta forma, necesitamos mantener una
  19:        // referencia a la conexión a nivel de clase para poder cerrarla despues!
  20:        return cmd.BeginExecuteReader(cb, state, CommandBehavior.CloseConnection);
  21:     }
  22:
  23:     void CargarGrilla(IAsyncResult ar) {
  24:        SqlDataReader reader = cmd.EndExecuteReader(ar);
  25:        this.GridView1.DataSource = reader
  26:        this.GridView1.DataBind();
  27:        reader.Close();
  28:     }
  29: }

Esta técnica puede aplicarse también a la ejecución de WebServices ya que los mismos ofrecen una forma Begin[WebMethod] y End[WebMethod] para cada uno de los métodos que se expongan.

Sin embargo, hay ocasiones en lo que deseamos hacer es algo más complejo que una simple consulta o simplemente el método ExecuteReader no nos sirve. En esos casos podemos hacer algo como lo siguiente:

   1: public partial class AsyncForm : System.Web.UI.Page {
   2:
   3:     // Referencia al EventHandler que vamos a usar para llamar al método que recupera los datos
   4:     private EventHandler ehRecuperarDatos = null;
   5:     // Aca vamos a guardar los datos que recuperemos
   6:     private DataSet dsDatos = null;
   7:
   8:     protected void Page_Load(object sender, EventArgs e) {
   9:
  10:         Page.AddOnPreRenderCompleteAsync(
  11:             new BeginEventHandler(this.RecuperarDatos),
  12:             new EndEventHandler(this.CargarGrilla)
  13:             );
  14:     }
  15:
  16:     IAsyncResult RecuperarDatos(object sender, EventArgs e, AsyncCallback cb, object state) {
  17:
  18:         // Esto es necesario porque tenemos que devolver un IAsyncResult
  19:         this.ehRecuperarDatos = new EventHandler(this.RecuperarDataSet);
  20:         return this.ehRecuperarDatos.BeginInvoke(this, EventArgs.Empty, cb, null);
  21:     }
  22:
  23:     private void RecuperarDataSet(object sender, EventArgs e) {
  24:         DataSet ds = new DataSet();
  25:         /*...*/
  26:         this.dsDatos = ds;
  27:     }
  28:
  29:     void CargarGrilla(IAsyncResult ar) {
  30:         this.ehRecuperarDatos.EndInvoke(ar);
  31:         this.GridView1.DataSource = this.dsDatos;
  32:         this.GridView1.DataBind();
  33:     }
  34: }

En el ejemplo estoy usando un EventHandler genérico, pero tranquilamente podríamos crear nuestro propio EventHandler con parámetros propios y usarlo para facilitarnos la tarea de recuperación y/o procesamiento de los datos.

Como se ve, no es muy difícil ejecutar tareas en segundo plano, pero éste método nos limita a sólo una única tarea. En un próximo post voy a mostrar otro método para ejecutar más de una tarea asincrónicamente.

[Update: Pueden encontrar la segunda parte de este artículo acá.]

¡Nos vemos en el próximo post!

Publicado originalmente en https://gerardocontijoch.wordpress.com.

Una respuesta to “Ejecutar tareas de forma asincrónica en ASP.NET 2.0 (Parte 1)”

  1. Jorge Andrés Cañón Sierra said

    Cordial saludo.

    Resulta que tengo un web service, deseo que luego de consumirlo, se ejecute otro método con el objetivo de hacer cambios en la base de datos.

    Esto es porque tengo muchos web services y deseo agregar una tarea común para todos cuando terminan su ejecución.

    Todos los aportes son bienvenidos.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

 
A %d blogueros les gusta esto: