[C#] Cómo hacer llamadas asíncronas usando Delegates.


Uno de los usos más comunes de los delegados son las llamadas a métodos asíncronas, esto es, se puede realizar una invocación a un método y volver al hilo principal inmediatamente mientras el delegado se ejecuta en un hilo separado. El delegado se ejecuta en paralelo al hilo llamador y cuando dicho delegado termina su trabajo, se realiza una llamada callback al llamante informando de su finalización.

¿Cómo se realizan las llamadas asíncronas?

Para realizar las llamadas asíncronas a través de un delegado, se emplea BeginInvoke y EndInvoke. Para ello veremos las definiciones que da la MSDN:

  • El método BeginInvoke inicia la llamada asincrónica. Tiene los mismos parámetros que el método que desea ejecutar de forma asincrónica, más dos parámetros opcionales adicionales. El primer parámetro es un delegado AsyncCallback que hace referencia a un método que se habrá de llamar cuando finalice la llamada asincrónica. El segundo parámetro es un objeto definido por el usuario que pasa información al método de devolución de llamada. BeginInvoke vuelve inmediatamente y no espera que se complete la llamada asincrónica. BeginInvoke devuelve IAsyncResult, que se puede usar para supervisar el progreso de la llamada asincrónica.
  • El método EndInvoke recupera los resultados de la llamada asincrónica. Se puede llamar en cualquier momento después de ejecutar BeginInvoke. Si la llamada asincrónica no ha completado, EndInvoke bloquea el subproceso que realiza la llamada hasta que se completa. Entre los parámetros de EndInvoke se incluyen los parámetros out y ref (<Out> ByRef y ByRef en Visual Basic) del método que desea ejecutar de forma asincrónica, además de la interfaz IAsyncResult devuelta por BeginInvoke.

NOTA: Se debe siempre llamar a EndInvoke cuando se termine la ejecución de la llamada asincrónica.

Existen varias formas de usar los métodos BeginInvoke y EndInvoke que veremos con ejemplos a continuación.

«Fire and Forget».

Esta forma es cuando se requiere realizar una llamada asíncrona a un método pero el cual no deseamos saber si ha terminado o no la invocación asincrónica.

using System;
using System.Threading;

public delegate void MiDelegadoAsincrono();

public class Test
{
 public static void Main()
 {
   // instanciamos un delegado.
   MiDelegadoAsincrono delegado = MiMetodo;

   Console.WriteLine("1.- Llamamos al delegado asíncronamente");
   delegado.BeginInvoke(null, null);
   Console.WriteLine("4.- Termina la ejecución del programa");

   Console.ReadKey();
 }

 private static void MiMetodo()
 { 
   Console.WriteLine("2.- Antes de pausar el hilo");
   // pausar la ejecución del hilo 1 seg.
   Thread.Sleep(1000);
   Console.WriteLine("3.- Después de pausar el hilo");
 }
}

Obtenemos la siguiente salida del programa:

1.- Llamamos al delegado asíncronamente
4.- Termina la ejecución del programa
2.- Antes de pausar el hilo
3.- Después de pausar el hilo

Como vemos, al realizar la llamada a BeginInvoke, el control se devuelve al hilo llamador sin esperar a que el método MiMetodo() termine su ejecución.

Esperar con EndInvoke().

Llamar a BeginInvoke seguido de un EndInvoke provoca que se realice un bloqueo del subproceso principal que no vuelve hasta que se completa la llamada asíncrona. Pero… ¿Cómo hace el motor de ejecución de .NET para enlazar un BeginInvoke con un EndInvoke? Pues empleando para ello un IAsyncResult que permite hacer el seguimiento de la ejecución del método asíncrono.

using System;
using System.Threading;

public delegate void MiDelegadoAsincrono();

public class Test
{
 public static void Main()
 {
   // instanciamos un delegado.
   MiDelegadoAsincrono delegado = MiMetodo;

   Console.WriteLine("1.- Llamamos al delegado asíncronamente");
   IAsyncResult result = delegado.BeginInvoke(null, null);
   delegado.EndInvoke(result);
   Console.WriteLine("4.- Termina la ejecución del programa");

   Console.ReadKey();
 }

 private static void MiMetodo()
 { 
   Console.WriteLine("2.- Antes de pausar el hilo");
   // pausar la ejecución del hilo 1 seg.
   Thread.Sleep(1000);
   Console.WriteLine("3.- Después de pausar el hilo");
 }
}

Obtenemos la siguiente salida del programa:

1.- Llamamos al delegado asíncronamente
2.- Antes de pausar el hilo
3.- Después de pausar el hilo
4.- Termina la ejecución del programa

Esperar con EndInvoke() verificando si se ha completado el método.

Para verificar si se ha terminado la ejecución de un delegado asíncrono se debe consultar la propiedad Completed del IAsyncResult.

using System;
using System.Threading;

public delegate void MiDelegadoAsincrono();

public class Test
{
 public static void Main()
 {
   // instanciamos un delegado.
   MiDelegadoAsincrono delegado = MiMetodo;

   Console.WriteLine("1.- Llamamos al delegado asíncronamente");
   IAsyncResult result = delegado.BeginInvoke(null, null);
   // verificar si se ha completado.
   while (!result.IsCompleted)
   {
     Thread.Sleep(500);
     Console.WriteLine(".");
   }
   delegado.EndInvoke(result);
   Console.WriteLine("4.- Termina la ejecución del programa");

   Console.ReadKey();
 }

 private static void MiMetodo()
 { 
   Console.WriteLine("2.- Antes de pausar el hilo");
   // pausar la ejecución del hilo 1 seg.
   Thread.Sleep(1000);
   Console.WriteLine("3.- Después de pausar el hilo");
 }
}

Obtenemos la siguiente salida del programa:

1.- Llamamos al delegado asíncronamente
2.- Antes de pausar el hilo
.
.
.
.
.
.
.
.
.
.
3.- Después de pausar el hilo
.
4.- Termina la ejecución del programa

Esperar con objeto WaitHandle.

Empleando la propiedad AsyncWaitHandle de IAsyncResult se puede obtener un objeto WaitHandle que señaliza la finalización de la llamada asíncrona.

using System;
using System.Threading;

public delegate void MiDelegadoAsincrono();

public class Test
{
 public static void Main()
 {
   // instanciamos un delegado.
   MiDelegadoAsincrono delegado = MiMetodo;

   Console.WriteLine("1.- Llamamos al delegado asíncronamente");
   IAsyncResult result = delegado.BeginInvoke(null, null);

   // Esperar hasta que se señalice la terminación.
   result.AsyncWaitHandle.WaitOne(); 
   delegado.EndInvoke(result);
   // cerrar el WaitHandle.
   result.AsyncWaitHandle.Close();
   Console.WriteLine("4.- Termina la ejecución del programa");

   Console.ReadKey();
 }

 private static void MiMetodo()
 { 
   Console.WriteLine("2.- Antes de pausar el hilo");
   // pausar la ejecución del hilo 1 seg.
   Thread.Sleep(1000);
   Console.WriteLine("3.- Después de pausar el hilo");
 }
}

Obtenemos la siguiente salida del programa:

1.- Llamamos al delegado asíncronamente
2.- Antes de pausar el hilo
3.- Después de pausar el hilo
4.- Termina la ejecución del programa

Ejecutar un método Callback al finalizar la ejecución.

Para notificar que un método asíncrono ha terminado se puede utilizar un callback. Para ello se debe pasar como parámetro a BeginInvoke un delegado AsyncCallback. Utilizando la propiedad AsyncDelegate del interfaz IAsyncResult se puede recuperar el delegado que fue usado y llamar a EndInvoke.

using System;
using System.Runtime.Remoting.Messaging;
using System.Threading;

public delegate void MiDelegadoAsincrono();

public class Test
{
  public static void Main()
  {
    // instanciamos un delegado. Observa que el método MiMetodo cumple con la firma definida en el delegado
   MiDelegadoAsincrono delegado = MiMetodo;

   Console.WriteLine("1.- Llamamos al delegado asíncronamente");
   // creamos un parámetro AsyncCallback
   IAsyncResult result = delegado.BeginInvoke(new AsyncCallback(MiCallback), null);
 
   Console.WriteLine("4.- Termina la ejecución del programa"); 
 
   Console.ReadKey();
  }

  private static void MiMetodo()
  { 
    Console.WriteLine("2.- Antes de pausar el hilo");
    // pausar la ejecución del hilo 1 seg.
    Thread.Sleep(1000);
    Console.WriteLine("3.- Después de pausar el hilo");
  }

  private static void MiCallback(IAsyncResult ar) 
  {
    Console.WriteLine("5.- Callback");

    AsyncResult result = (AsyncResult)ar;

    // recuperamos el delegado para hacer EndInvoke
    MiDelegadoAsincrono delegado = (MiDelegadoAsincrono)result.AsyncDelegate;
    delegado.EndInvoke(ar); 
   }
}

Obtenemos la siguiente salida del programa:

1.- Llamamos al delegado asíncronamente
4.- Termina la ejecución del programa
2.- Antes de pausar el hilo
3.- Después de pausar el hilo
5.- Callback

Ya hemos visto cómo se utilizan los delegados para realizar llamadas a métodos asíncronas. Ahora veremos más cositas interesantes.

¿Cómo hace .NET para realizar las llamadas asíncronas?

Cada llamada a un delegado asíncrono se realiza en un hilo separado del ThreadPool. En realidad para el programador es transparente su uso pero se debe tener en cuenta lo siguiente:

  • El ThreadPool por defecto tiene 25 hilos disponibles aunque se puede modificar dicho límite.
  • Una vez que todos los hilos del ThreadPool se estén usando, la llamada asíncrona se queda encolada hasta que haya un hilo libre, por lo que provoca una pérdida de rendimiento. Para evitarlo se debe dimensionar bien el tamaño del pool.

¿Cómo se pasan parámetros a los delegados asíncronos?

En los ejemplos anteriores hemos utilizado los delegado más simples, sin valores de retorno y sin parámetros. Veamos entonces el uso de parámetros:

using System;
using System.Threading;

// modificamos el delegado para que tenga un parámetro de entrada int
// y devuelva un string
public delegate string MiDelegadoAsincrono(int i);

public class Test
{
  public static void Main()
  {
    // instanciamos un delegado.
    MiDelegadoAsincrono delegado = MiMetodo;

    Console.WriteLine("1.- Llamamos al delegado asíncronamente");
 
    // definimos los parámetros del delegado y se lo pasamos
    int i = 37;
    IAsyncResult result = delegado.BeginInvoke(i, null, null);
    // recuperamos la salida del método
    string salida = delegado.EndInvoke(result);
    
    Console.WriteLine("4.- " + salida);
    Console.WriteLine("5.- Termina la ejecución del programa");

    Console.ReadKey();
  }

  private static string MiMetodo(int i)
  { 
    Console.WriteLine("2.- Antes de pausar el hilo");
    // pausar la ejecución del hilo 1 seg.
    Thread.Sleep(1000);

    Console.WriteLine("3.- Después de pausar el hilo");

    return "Me gusta el blog System.OutOfMemoryException " + i.ToString();
  }
}

Obtenemos la siguiente salida del programa:

1.- Llamamos al delegado asíncronamente
2.- Antes de pausar el hilo
3.- Después de pausar el hilo
4.- Me gusta el blog System.OutOfMemoryException 37
5.- Termina la ejecución del programa

Si definimos en nuestro delegado asíncrono parámetros con el modificador out o ref, éstos parámetros se pueden recuperar desde EndInvoke. Veamos un ejemplo:

using System;
using System.Collections.Generic;
using System.Threading;

// modificamos el delegado para que tenga un parámetro de entrada int
// un parámetro out de tipo string y un parámetro ref de tipo List<string>
// y devuelva un string
public delegate string MiDelegadoAsincrono(int i, out string s, ref List<string> list);

public class Test
{
  public static void Main()
  {
    // instanciamos un delegado.
    MiDelegadoAsincrono delegado = MiMetodo;

    Console.WriteLine("1.- Llamamos al delegado asíncronamente");
 
    // definimos los parámetros del delegado y se los pasamos
    int i = 37;
    string s = string.Empty;
    List<string> list = new List<string>();

    IAsyncResult result = delegado.BeginInvoke(i, out s, ref list, null, null);
    // recuperamos los parámetros de salida
    string salida = delegado.EndInvoke(out s, ref list, result);
    Console.WriteLine("4.- " + salida);
    Console.WriteLine("5.- El parámetro out s tiene el valor " + s);
    Console.WriteLine("6.- El parámetro ref list tiene los valores " + string.Join(",", list));
    Console.WriteLine("7.- Termina la ejecución del programa");

    Console.ReadKey();
  }

  private static string MiMetodo(int i, out string s, ref List<string> list)
  { 
    Console.WriteLine("2.- Antes de pausar el hilo");
    // pausar la ejecución del hilo 1 seg.
    Thread.Sleep(1000);

    Console.WriteLine("3.- Después de pausar el hilo");
    s = "Prueba de parámetro OUT";
    list = new List<string>() { "A", "B", "C" };
 
    return "Me gusta el blog System.OutOfMemoryException " + i.ToString();
  }
}

Obtenemos la siguiente salida del programa:

1.- Llamamos al delegado asíncronamente
2.- Antes de pausar el hilo
3.- Después de pausar el hilo
4.- Me gusta el blog System.OutOfMemoryException 37
5.- El parámetro out s tiene el valor Prueba de parámetro OUT
6.- El parámetro ref list tiene los valores A, B, C
7.- Termina la ejecución del programa

¿Cómo capturamos las excepciones?

Para capturar las excepciones debemos saber que el bloque try/catch se aplica al método EndInvoke, ya que BeginInvoke lo único que hace es iniciar el método en el ThreadPool.

Veamos un ejemplo de cómo capturar las excepciones:

using System;
using System.Threading;

public delegate void MiDelegadoAsincrono();

public class Test
{
  public static void Main()
  {
    // instanciamos un delegado.
    MiDelegadoAsincrono delegado = MiMetodo;

    Console.WriteLine("1.- Llamamos al delegado asíncronamente");
    IAsyncResult result = delegado.BeginInvoke(null, null);
    try
    {
      delegado.EndInvoke(result);
    }
    catch (Exception e)
    {
      // escribir mensaje de error
      Console.WriteLine(e.Message);
    }

    Console.WriteLine("4.- Termina la ejecución del programa");

    Console.ReadKey();
  }

  private static void MiMetodo()
  { 
     Console.WriteLine("2.- Antes de pausar el hilo");
    // pausar la ejecución del hilo 1 seg.
    Thread.Sleep(1000);
    Console.WriteLine("3.- Después de pausar el hilo");

    // lanzar una excepción
    throw new Exception("MiMetodo ha lanzado una excepción");
  }
}

Obtenemos la siguiente salida del programa:

1.- Llamamos al delegado asíncronamente
2.- Antes de pausar el hilo
3.- Después de pausar el hilo
MiMetodo ha lanzado una excepción
4.- Termina la ejecución del programa

Hemos aprendido en este post a manejar llamadas asíncronas a través de los delegate. Os dejo un enlace de referencia de la MSDN Asynchronous Programming Using Delegates

Un comentario en “[C#] Cómo hacer llamadas asíncronas usando Delegates.

Deja un comentario