Realización de llamadas asíncronas a delegados

Los delegados se utilizan en muchos contextos dentro de .NET, permitiendo básicamente realizar llamadas a métodos de forma dinámica, de forma muy similar a como en otros lenguajes se utilizan los punteros a funciones. Sin embargo la potencia de los delegados es mucho mayor, al ofrecernos una estricta comprobación de tipos, posibilidad de asignar varios métodos a un mismo delegado, y el tema sobre el que voy a tratar a continuación, que es la realizacón de llamadas asíncronas.

Pero antes de nada hay que explicar a qué se refiere esto de «llamadas asíncronas». Cuando creamos un delegado a un método y lo utilizamos, lo que se hace es llamar a ese método de forma muy similar a como lo haríamos si hubiésemos escrito directamente la llamada en código, por lo que el control de la aplicación se pasa al método y se queda allí hasta que termina su ejecución y regresa, es decir, exactamente igual a como funcionan las llamadas a métodos normales. En cambio en las «llamadas asíncronas» la llamada al método se realiza en un hilo de ejecución diferente, por lo que inmediatamente después de realizar la llamada, el programa continúa con su ejecución y en paralelo con la del método. Esto plantea varias preguntas, ¿cómo se crea ese nuevo hilo de ejecución?, ¿por qué no utilizar directamente un Thread?, ¿cómo nos podemos enterar de cuando termina la ejecución del método?, ¿cómo podemos obtener el valor de retorno devuelto por el método?. A todas estas preguntas intentaré contestar a continuación.

Vamos a explicar las diferentes alternativas partiendo de un ejemplo básico de utilización de un delegado al cual iremos añadiendo las diferentes cuestiones planteadas.

using System;
 
namespace esl.Delegados
{
    class Class1
    {
        [STAThread]
        static void Main(string[] args)
        {
            MyClass mc = new MyClass();
 
            // creo el delegado y realizo la llamada
            CalculoHandler myDelegate1 = new CalculoHandler(mc.Calculo1);
            myDelegate1(1513);
 
            // creo un segundo delegado y lo llamo
            CalculoHandler myDelegate2 = new CalculoHandler(mc.Calculo2);
            myDelegate2(1513);
 
            Console.WriteLine("Estamos en el nivel principal");
            Console.Read();
        }
    }
 
    delegate int CalculoHandler(int n);
 
    class MyClass
    {
        public int Calculo1(int n)
        {
            Console.WriteLine("Comienza Calculo1 para "+n);
            System.Threading.Thread.Sleep(10000);
            Console.WriteLine("Finaliza Calculo1 para "+n);
            return 1234;
        }
 
        public int Calculo2(int n)
        {
            Console.WriteLine("Comienza Calculo2 para "+n);
            System.Threading.Thread.Sleep(10000);
            Console.WriteLine("Finaliza Calculo2 para "+n);
            return 5678;
        }
    }
}

En el anterior código se crea una delegado, definido como delegate int CalculoHandler(int n); que admite un valor entero como parámetro y que devuelve también un valor de tipo entero. Lo que hace el programa es crear dos delegados, uno para el método Calculo1 y otro para el método Calculo2, y realizar las llamadas. En estos métodos he añadido un delay de 10 segundos simulando un cálculo complejo, de forma que más adelante podamos apreciar la diferencia existente en ejecutar las llamadas de forma asíncrona. En esta ocasión, al hacerse estas llamadas de forma asíncrona el resultado de la ejecución dura 20 segundos y es el siguiente:

Comienza Calculo1 para 1513
Finaliza Calculo1 para 1513
Comienza Calculo2 para 1513
Finaliza Calculo2 para 1513
Estamos en el nivel principal

Como puede verse la llamada a Calculo2 se realiza una vez finalizada la llamada a Calculo1. Ahora vamos a modificar el programa de forma que las llamadas a los delegados se realicen de forma asíncrona. Para ello lo que haremos será modificar las llamadas utilizadas en el código anterior myDelegate1(1513); por myDelegate2.BeginInvoke(1513,null,null);, que utiliza dos parámetros extra que por ahora dejaremos a null. Utilizando BeginInvoke ya no llamamos directamente al delegado, y como puede verse en la salida mostrada a continuación, las llamadas a los métodos se realizan de forma asíncrona.

Comienza Calculo1 para 1513
Comienza Calculo2 para 1513
Estamos en el nivel principal
Finaliza Calculo1 para 1513
Finaliza Calculo2 para 1513

A diferencia del primer caso con llamadas síncronas, en la salida anterior se ve como no se espera a que finalice la llamada a Calculo1 para entrar en Calculo2, y que tras esta segunda llamada se continúa con el flujo principal mostrando el mensaje Estamos en el nivel principal, tras lo cual aparecen los mensajes de finalización de los cálculos, obteniendo un tiempo total de ejecución de 10 segundos, ya que los delays de ambos métodos se han realizado en paralelo. Hay que tener en cuenta que la salida anterior puede variar entre ejecuciones en función del tiempo que le cueste en cada ejecución crear los diferentes hilos, por lo que las 3 primeras líneas pueden aparecer en diferente orden.

Con esto ya hemos respondido a la primera pregunta, ¿cómo se crean los hilos de ejecución?. La respuesta es que de forma completamente transparente mediante la utilización de BeginInvoke. En realidad lo que sucede es que que .NET dispone de un pool de threads (25 por procesador) que se utilizan para este tipo de cosas, por lo que al hacer una llamada a BeginInvoke se hace que uno de estos threads la ejecute.

La segunda pregunta hacía referencia a por qué no utilizar directamente un thread y la respuesta es que aparte de que ya lo estamos utilizando, aunque de forma transparente, un thread nos permite realizar una llamada a un método sin parámetros de forma muy sencilla, pero si queremos pasarle parámetros u obtener su resultado la cosa se complica un poco, de todas formas si te interesa profundizar algo más en esto, lo tengo explicado en Creación de hilos con parámetros en C#.

Una vez explicado cómo realizar las llamadas de forma asíncrona vamos a ver cómo enterarnos de cuándo finaliza la ejecución de cada método y de cómo obtener sus resultados, para lo cual utilizaremos los dos parámetros que hemos dejado antes como null en BeginInvoke.

using System;
 
namespace esl.Delegados
{
    class Class1
    {
        [STAThread]
        static void Main(string[] args)
        {
            MyClass mc = new MyClass();
 
            // creo el delegado y realizo la llamada
            CalculoHandler myDelegate1 = new CalculoHandler(mc.Calculo1);    
            myDelegate1.BeginInvoke(1513,
                new AsyncCallback(CalculoCallback),myDelegate1);
 
 
            // creo un segundo delegado y lo llamo
            CalculoHandler myDelegate2 = new CalculoHandler(mc.Calculo2);
            myDelegate2.BeginInvoke(1513,
                new AsyncCallback(CalculoCallback),myDelegate2);
 
            Console.WriteLine("Estamos en el nivel principal");
            Console.Read();
        }
 
        static void CalculoCallback(IAsyncResult ar)
        {
            CalculoHandler asyncDelegate = (CalculoHandler)ar.AsyncState; 
            int n = asyncDelegate.EndInvoke(ar);
            Console.WriteLine("Capturada finalización de "
                +asyncDelegate.Method.Name
                +" con resultado: "+n);
        }
    }
 
    delegate int CalculoHandler(int n);
 
    class MyClass
    {
        public int Calculo1(int n)
        {
            Console.WriteLine("Comienza Calculo1 para "+n);
            System.Threading.Thread.Sleep(10000);
            Console.WriteLine("Finaliza Calculo1 para "+n);
            return 1234;
        }
 
        public int Calculo2(int n)
        {
            Console.WriteLine("Comienza Calculo2 para "+n);
            System.Threading.Thread.Sleep(10000);
            Console.WriteLine("Finaliza Calculo2 para "+n);
            return 5678;
        }
    }
}

La modificación principal hecha en el código se trata de la inclusión del método CalculoCallback, y la modificación de las llamadas a BeginInvoke para que sustituyan los parámetros null de la forma myDelegate1.BeginInvoke(1513, new AsyncCallback(CalculoCallback),myDelegate1);. El penúltimo parámetro de la llamada a BeginInvoke corresponde a un delegado al que se llamará tras finalizar la ejecución del método y el último parámetro se corresponde con cualquier información del contexto actual que queramos hacer disponible en ese método. En nuestro caso hemos indicado que queremos utilizar como callback el método CalculoCallback, el cual hemos creado de forma acorde al delegado AsyncCallback, haciendo que devuelva void y que acepte un parámetro IAsyncResult. Como información de estado proporcionamos el propio delegado.

Con todo esto al finalizar cada una de las ejecuciones de los métodos, el hilo desde el que se esté ejecutando realiza una llamada al método indicado en el callback. Dentro de este método en nuestro caso lo que queremos es obtener el valor devuelto por los métodos Calculo, para lo cual necesitamos llamar al método EndInvoke del delegado. Como el método CalculoCallback recibe únicamente un IAsyncResult, es por ello que en la llamada a BeginInvoke hemos puesto como último parámetro el propio delegado. De esta forma dentro de CalculoCallback podemos obtener el delegado a partir del que hemos llegado ahí mediante CalculoHandler asyncDelegate = (CalculoHandler)ar.AsyncState;. Tras esto podemos obtener el valor devuelto por el método original Calculo mediante int n = asyncDelegate.EndInvoke(ar);.

Puede comprobarse a continuación como las llamadas al método CalculoCallback se realizan tras finalizar los cálculos.

Estamos en el nivel principal
Comienza Calculo1 para 1513
Comienza Calculo2 para 1513
Finaliza Calculo1 para 1513
Finaliza Calculo2 para 1513
Capturada finalización de Calculo2 con resultado: 5678
Capturada finalización de Calculo1 con resultado: 1234

Es habitual cuando se realizan llamadas mediante BeginInvoke utilizar como último parámetro el propio delegado para que esté disponible en el método callback, pero no es ni mucho menos obligatorio, de hecho, BeginInvoke acepta cualquier objeto, por lo que podemos pasar al método callback cualquier información que consideremos que vaya a necesitar. En cualquier caso sí que suele ser útil enviar el delegado tal como hemos visto en el ejemplo, por lo que si quisiéramos enviar más información, podríamos utilizar por ejemplo un ArrayList para guardar en la posición 0 el delegado, y en el resto de posiciones lo que necesitemos. En nuestro ejemplo, vamos a imaginarnos que queremos mostrar en callback el valor original enviado al método de cálculo, podríamos que modificar la llamada a BeginInvoke de la siguiente forma:

int n2 = 1513;
CalculoHandler myDelegate2 = new CalculoHandler(mc.Calculo2);
ArrayList al2 = new ArrayList();
al2.Add(myDelegate2);
al2.Add(n2);
myDelegate2.BeginInvoke(n2,
    new AsyncCallback(CalculoCallback),al2);

Habría que modificar también por lo tanto CalculoCallback.

static void CalculoCallback(IAsyncResult ar)
{
    ArrayList al = (ArrayList)ar.AsyncState;
    CalculoHandler asyncDelegate = (CalculoHandler)al[0]; 
    int param = (int)al[1]; 
    int n = asyncDelegate.EndInvoke(ar);
    Console.WriteLine("Capturada finalización de "
        +asyncDelegate.Method.Name+"("+param+")"
        +" con resultado: "+n);
}

Con esto han quedado ya respondidas todas las preguntas planteadas al comienzo, he explicado cómo realizar las llamadas asíncronas, el motivo de no utilizar threads directamente, la forma de enterarnos de cuándo termina la ejecución de las llamadas asíncronas, y cómo obtener los resultados de estas llamadas. A partir de aquí no queda más que practicar, la verdad es que la primera vez que se ve esto puede parecer algo complejo, pero conforme se utiliza unas pocas veces se va viendo toda su potencia y que al final, tampoco es tan difícil…

Twitter Digg Delicious Stumbleupon Technorati Facebook Email

11 Respuestas para “Realización de llamadas asíncronas a delegados”

  1. Gracias por el artículo. Ha sido muy interesante. 😉

  2. Gracias…! muy útil y bien explicado. En 5 minutos tenía todo funcionando en mi proyecto.

  3. Sergio Ivan Mendoza 23. Mar, 2007 en 8:40 pm

    Excelente articulo muy bien explicado, conciso, muchas felicidaes

  4. Muchas gracias, una excelente explicacion a las llamadas asincronas.

  5. Muy buena explicación, solo tengo una duda habra alguna forma de ponerle pausa al proceso, para después volverlo a levantar.
    Gracias

  6. Un articulo muy profesional, estoy desarrollando un webservice en el que necesito por un lado respuestas inmediatas de las llamadas, y por otro lado confirmaciones de operaciones con dispositivos (impresioón, configuración ,etc).
    Gracias

  7. Muchas gracias, por el articulo aclaro todas mis dudas … ya que recien inicio en C# y realmente me fue de gran ayuda 🙂

  8. Felicidades, eres muy bueno, aunque no me quedo bien claro el Callback, todo lo demas esta de maravilla, la verdad es que ya llebo como 2 meses estudiando la asincronia pero nada entendia, y con este articulo, todo me fue de maravilla, mil gracias

  9. puedes pasrlo a vb.net por favor?
    Gracias.

  10. CARAY, NI DUDA QUEDA, EXCELENTISIMO, SIGAN ASI PORFA. GRACIAS DE NUEVO.

  11. Excelente información, tenía tiempo buscando alguien/algo que me explicara tan claramente como acá. Gracias! =)