El blog del burgués

23 enero 2010

Patrones asíncronos en C#: El patrón IAsyncResult

Filed under: C# — elburgues @ 11:57 PM

Utilizar el espacio de nombres System.Threading no es la única manera de construir aplicaciones .NET multitarea. Los delegados también tienen la habilidad de invocar miembros asíncronamente y aunque las dificultades para construir una aplicación multiproceso robusta no han desaparecido completamente, los delegados hacen de esto algo mucho más sencillo. Recordando, un delegado es esencialmente un puntero a una función con seguridad de tipos incluida. Cuando se declara un delegado, el compilador de C# responde creando una clase sellada que deriva de la clase System.MulticastDelegate (la cual, a su vez, deriva de la clase System.Delegate).Consideremos el siguiente ejemplo:

// Necesario para la llamada de Thread.Sleep().
using System.Threading;
using System;
namespace DelegadoSincrono
{
  public delegate int Operacion(int x, int y);
  class Program
  {
    static void Main(string[] args)
    {
        // Imprimimos el Id del hilo principal de la aplicación
      Console.WriteLine(“Main() invocado sobre el hilo {0}.”, Thread.CurrentThread.ManagedThreadId);
        // Invocamos a la funcion Suma de manera síncrona
      Operacion b = new Operacion(Suma);
      // también podríamos escribir b.Invoke(10, 10);
      int respuesta = b(10, 10);
      // Las siguientes líneas de código no ejecutarán hasta que el método Suma no se haya completado
      Console.WriteLine(“Seguimos trabajando en Main()!”);
      Console.WriteLine(“10 + 10 es {0}.”, respuesta);
      Console.ReadLine();
    }
    #region Operación suma que consume mucho tiempo
    static int Suma(int x, int y)
    {
        // Imprimimos el Id del hilo principal de la aplicación
        Console.WriteLine(“Suma() invocado sobre el hilo {0}.”, Thread.CurrentThread.ManagedThreadId);
      // Hacemos una pausa con el objetivo de simular una operación que lleve mucho tiempo realizar.
      Thread.Sleep(5000);
      return x + y;
    }
    #endregion
  }
}

Al ejecutar, sucede lo siguiente:

Lo que ha pasado es que se ha invocado de manera síncrona a la función Suma mantenida por el delegado Operacion. Entonces, el hilo llamante (el hilo principal de ejecución de la aplicación) se ha visto obligado a esperar cinco segundos hasta que la ejecución del método Suma ha terminado (la invocación síncrona de métodos son llamadas bloqueantes para la aplicación). Como se puede ver en la imagen anterior, todo el trabajo que esta aplicación hace, está realizado por el hilo principal (nº 11 en este caso). Estamos haciendo uso de System.Threading solamente para tener acceso al identificador del hilo y al método Sleep.

La naturaleza asíncrona de los delegados

Puede que lo que nos ha pasado en el párrafo anterior no nos importe en la mayoría de las ocasiones, pero puede ocurrir que una llamada a un método, por la tarea que éste tenga que realizar, tarde mucho tiempo en realizarse, dejándonos la aplicación completamente bloqueada, sin responder a cualquier elemento de la interfaz de usuario. La buena noticia es que usando delegados, automáticamente se nos provee la manera de hacer que esas llamadas se ejecuten en un hilo diferente al hilo principal de la aplicación, sin ser necesario que conozcamos los detalles del espacio de nombres System.Threading. Observemos ahora el siguiente código:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace DelegadoAsincrono
{
  public delegate int Operacion(int x, int y);
      class Program
      {
        static void Main(string[] args)
        {
          // Imprimimos el Id del hilo principal de la aplicación
          Console.WriteLine((“Main() invocado sobre el hilo {0}.”), Thread.CurrentThread.ManagedThreadId);
          // IInvocamos a la función Suma en un hilo diferente al hilo principal de la aplicación.
          Operacion b = new Operacion(Suma);
          IAsyncResult iftAR = b.BeginInvoke(10, 10, null, null);
          // Este mensaje se imprimirá en la consola una vez por segundo
          // hasta que la ejecución del método Suma haya terminado.
          while (!iftAR.AsyncWaitHandle.WaitOne(1000, true))
          {
            Console.WriteLine(“¡Seguimos trabajando en Main()!”);
          }
          // Obtemos el resultado del método Suma ahora que la respuesta ya está lista.
          int respuesta = b.EndInvoke(iftAR);
          Console.WriteLine(“10 + 10 es {0}.”, respuesta);
          Console.ReadLine();
        }
        #region Operación suma que consume mucho tiempo
        static int Suma(int x, int y)
        {
            // Imprimimos el Id del hilo principal de la aplicación
            Console.WriteLine(“Suma() invocado sobre el hilo {0}.”,
            Thread.CurrentThread.ManagedThreadId);
          // Hacemos una pausa con el objetivo de simular una operación que lleve mucho tiempo realizar.
          Thread.Sleep(5000);
          return x + y;
        }
        #endregion
      }
}

Se produce la siguiente salida:

Hemos sido capaces de hacer que la tarea que tarda cinco segundos en realizarse se ejecute en otro hilo diferente. Esto ha hecho que nuestra aplicación, mientras tanto, no se haya quedado bloqueada. Cuando la tarea ha terminado de realizarse, nos hemos dado cuenta de ello y hemos capturado el resultado. ¿Cómo lo hemos hecho? Usando la dualidad Begin…/End… Observa la siguiente figura:

Al haber declarado a ‘b’ como una variable del tipo Operacion, automáticamente tenemos a nuestra disposición, entre otras cosas, a los métodos BeginInvoke/EndInvoke, que se encargan precisamente de hacer llamadas asíncronas (en nuestro caso al método Suma que mantiene nuestro delegado Operacion).

Es importante reseñar que el primer “set” de parámetros a suministrar al método BeginInvoke no siempre es igual, es decir, depende directamente del prototipo del delegado, en nuestro caso, dos parámetros enteros de entrada (de momento, a los dos últimos parámetros de BeginInvoke pasamos el valor null). Con la función EndInvoke pasa lo mismo, en nuestro caso, lo que devuelve será otro entero (como marca el delegado Operacion).

El método BeginInvoke siempre devuelve un objeto que implementa la interfaz IAsyncResult, mientras que EndInvoke requiere que se le suministre dicho objeto como único parámetro. Esto permite al hilo llamante obtener el resultado (cuando ya esté disponible, que para saberlo usamos IAsyncResult.AsyncWaitHandle.WaitOne) de la invocación asíncrona de la tarea mediante EndInvoke. La interfaz IAsyncResult (definida en el espacio de nombres System), está definida así:

public interface IAsyncResult
{
object AsyncState { get; }
WaitHandle AsyncWaitHandle { get; }
bool CompletedSynchronously { get; }
bool IsCompleted { get; }
}
 

Si lo que marca la definición del delegado es un método que devuelve un void, entonces no necesitarás llamar a EndInvoke. La manera en la que estamos sincronizando el hilo llamante con el hilo que realiza la tarea de larga duración es mediante esta porción de código:

          while (!iftAR.AsyncWaitHandle.WaitOne(1000, true))
          {            
            Console.WriteLine(“¡Seguimos trabajando en Main()!”);
          }

La interfaz IAsyncResult provee la propiedad AsyncWaitHandle, la cual devuelve un objeto del tipo WaitHandle, el cual expone un método llamado WaitOne. El beneficio que tiene esto es que se puede especificar el tiempo máximo que estás dispuesto a esperar a que la tarea de larga duración termine. Si el tiempo límite se excede WaitOne devuelve false. Esto lo digo porque otra forma de haber conseguido lo mismo hubiera sido esta:

          while (!iftAR.IsCompleted)
          {
            Console.WriteLine(“¡Seguimos trabajando en Main()!”);
            Thread.Sleep(1000);
          }

El delegado System.AsyncCallback

Afortunadamente los delegados nos proveen con técnicas más “elegantes” para conseguir la sincronización. En vez de estar haciendo un polling de preguntas desde el hilo principal, es más eficiente que sea el hilo secundario el que informe al principal de que la tarea ha terminado. Para habilitar este comportamiento, es necesario suministrar como parámetro a la función BeginInvoke una instancia del delegado System.AsyncCallback (dónde hasta ahora hemos estado pasando null), el cual, como buen delegado, solo puede invocar métodos cuyo prototipo coincida con el que él define, a saber:

public delegate void AsyncCallback(System.IAsyncResult ar)
 
Observemos ahora como queda nuestro programa:
 
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Runtime.Remoting.Messaging;
namespace DelegadoAsincrono
{
      public delegate int Operacion(int x, int y);
      class Program
      {
            static void Main(string[] args)
            {
              Console.WriteLine((“Main() invocado sobre el hilo {0}.”), Thread.CurrentThread.ManagedThreadId);
              Operacion b = new Operacion(Suma);
              b.BeginInvoke(10, 10, new AsyncCallback(SumaCompletada),
              “Main() te agradece que sumes estos números.”);
              // Supongamos que otro trabajo se lleva a cabo aquí…
              Console.ReadLine();
            }
            static void SumaCompletada(IAsyncResult itfAR)
            {
              Console.WriteLine(“SumaCompletada() invocado sobre el hilo {0}.”, Thread.CurrentThread.ManagedThreadId);
              Console.WriteLine(“Tu suma ha sido realizada”);
              // Ahora obtenemos el resultado
              AsyncResult ar = (AsyncResult)itfAR;
              Operacion b = (Operacion)ar.AsyncDelegate;
              Console.WriteLine(“10 + 10 es {0}.”, b.EndInvoke(itfAR));
              //Recuperamos el objeto informador y hacemos un casting a un string
              string msg = (string)itfAR.AsyncState;
              Console.WriteLine(msg);
            }
            static int Suma(int x, int y)
            {
              Console.WriteLine(“Suma() invocado en el hilo {0}.”,
              Thread.CurrentThread.ManagedThreadId);
              Thread.Sleep(5000);
              return x + y;
            }
      }
}

Y lo que sucede ahora al ejecutar es esto:

Si te fijas, ahora no usamos EndInvoke, ni usamos para nada en Main() lo que BeginInvoke devuelve. Pasamos new AsyncCallback(SumaCompletada) como parámetro a BeginInvoke, es como decir: “…vete haciendo esta operación y cuando termines, dispara SumaCompletada y pásale como parámetro el resultado del tipo IAsyncResult ”. Vamos comentando SumaCompletada:

El parámetro de entrada IAsyncResult es una instancia de la clase AsyncResult, definida en el espacio de nombres System.Runtime.Remoting.Messaging:

AsyncResult ar = (AsyncResult)itfAR;

Si queremos obtener una referencia al delegado Operacion alojado en Main(), simplemente hay que hacer un casting del System.Object devuelto por la propiedad estática AsyncDelegate al tipo Operacion:

Operacion b = (Operacion)ar.AsyncDelegate;

Y ahora ya podemos llamar a EndInvoke:

Console.WriteLine(“10 + 10 es {0}.”, b.EndInvoke(itfAR));

El ultimo parámetro de BeginInvoke, que también ha estado siendo null hasta ahora, es del tipo System.Object y te permite pasar información adicional desde el hilo principal. Al ser del tipo System.Object, puedes pasar cualquier tipo de parámetro que quieras, mientras que lo tengas en cuenta a la hora de hacer el casting:

string msg = (string)itfAR.AsyncState;
Console.WriteLine(msg);
 

.NET Framework define dos patrones para exponer un comportamiento asíncrono. Uno es el que acabamos de ver. El otro es el patrón asíncrono basado en eventos que es el que recomienda microsoft que hay que usar siempre que se pueda, aunque sea menos eficiente. Además, hemos visto hoy que el callback se dispara en un hilo diferente al hilo principal de la aplicación. Cuando se obtienen resultados que quizás interesen mostar en la interfaz de usuario, pues es un problema porque no se puede acceder desde un hilo a los controles que han sido creados dentro de otro hilo (se lanzaría una excepción), o al menos, no directamente. Este tema en el patrón asíncrono basado en eventos está contemplado, patrón que ya veremos en otra entrada más adelante.

 
 
Anuncios

2 comentarios »

  1. Muy interesante e instructivo, como siempre, el artículo. Lo cierto es el trabajo asíncrono y el manejo de hebras resulta siempre algo delicado. Sin embargo, cada vez es más necesario controlarlo adecuadamente, especialmente cuando los procesadores multinúcleo están a la orden del día, Visual Studio 2010 nos viene repletito de herramientas de depuración para este tipo de aplicaciones y hay proyectos “de laboratorio” de Microsoft como CHESS o Axum que tan buena pinta tienen. Habrá que investigarlos, ¿verdad?

    Un saludote, apañero.

    Comentario por Lobosoft — 30 enero 2010 @ 10:17 AM | Responder

    • ¡Muchas gracias compañero!

      Tengamos todos la suerte de que el autor de ese magnífico blog llamado lobosoft se motive un día a compartir con nosotros todas sus experiencias relativas al manejo de hebras con Visual Studio 2010. Sin duda alguna, se tratarán de entradas ¡para no perderse!

      Comentario por elburgues — 30 enero 2010 @ 10:50 AM | Responder


RSS feed for comments on this post. TrackBack URI

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

Crea un blog o un sitio web gratuitos con WordPress.com.

A %d blogueros les gusta esto: