La programación asíncrona es un esquema de programación que puede mejorar mucho el rendimiento de tus desarrollos, si lo aplicas correctamente.
Por ejemplo, a veces nuestros aplicativos se quedan congelados cuando el usuario interactúa con ellos. Esto ocurre porque hay algunas tareas pesadas que se están ejecutando, y como sólo hay un hilo de ejecución, no le queda otra que esperar a que termine dicha tarea para estar nuevamente disponible. Mejorar este tipo de comportamientos puede darle un toque de profesionalidad a tus aplicativos que siempre es importante.
Hay varias formas de implementar la programación asíncrona. El día de hoy veremos la primera: Delegados asíncronos.
¿Qué es un Delegado?
Un delegado es la implementación de .NET de los punteros a funciones. Un delegado es un tipo de dato, por lo que se puede instanciar, y a través de estas variables llamar a funciones que tienen la misma estructura (tipo de datos de retorno y parámetros) que el delegado.
Vamos con un ejemplo (muy) sencillo. Declaramos un delegado:
delegate int delegadoProducto(int a, int b);
Luego creamos una función con la misma estructura del delegado (ojo que la creamos en una clase separada):
public class DelegadoSimple { public int Producto(int x, int y) { Console.WriteLine(); Console.WriteLine("DelegadoSimple.Producto: Calculando el producto de {0} por {1}", x.ToString(), y.ToString()); Console.WriteLine(); return x * y; } }
Ahora instanciamos un objeto de nuestro delegado, asignándole la función que creamos en el paso anterior; y lo ejecutamos.
public static void Main(string[] args) { // Instanciamos un delegado var objDelegado = new DelegadoSimple(); // Asignamos el objeto recién creado a la función que creamos en la clase de apoyo delegadoProducto variableDelegadoProducto = new delegadoProducto(objDelegado.Producto); // Llamamos a la función a través del delegado var resultado = variableDelegadoProducto(2, 3); // Imprimimos el resultado de la ejecución Console.WriteLine("Resultado del producto es: {0}", resultado.ToString()); Console.ReadLine(); }
Como ven, sólo se puede asignar funciones a objetos que tienen la misma estructura que su delegado.
La salida de la ejecución es la siguiente:
Podría parecer que este esquema es innecesario, ya que se puede llamar directamente a la función sin necesidad de usar un delegado, pero recuerda que es un ejemplo simple para fijar conceptos. Los delegados son muy usados para la gestión de eventos, llamadas asíncronas, accesos más permisivos en diferentes hilos de ejecución e incluso métodos anónimos. Puedes encontrar más información de delegados en este enlace o en este otro enlace.
Llamada síncrona a un delegado
Ahora que ya vimos cómo crear un delegado y cómo usarlo, vamos con un ejemplo un poco más elaborado para entender mejor el concepto de llamada síncrona.
Para este ejemplo vamos a usar a nuestros amigos jedis Anakin y Obi Wan. Ellos harán un viaje interplanetario que les demora 5 segundos aproximadamente. Es lo bueno de la velocidad de la luz.
Tenemos el siguiente código:
using System; using System.Diagnostics; namespace ProgramacionAsincrona.Delegados { class Program { delegate int DelegadoViajeInterplanetario(string NombreViajero, Stopwatch temporizador); static void Main(string[] args) { // Se instancia un temporizador para calcular el tiempo de ejecución. var temporizador = Stopwatch.StartNew(); // Se instancia la clase de apoyo var objViaje = new Viaje(); // Se crea el primer delegado, que representará el primer viaje. DelegadoViajeInterplanetario viajeAnakin = new DelegadoViajeInterplanetario(objViaje.ViajeInterplanetario); // Se crea el segundo delegado, que representará el segundo viaje. DelegadoViajeInterplanetario viajeObiWan = new DelegadoViajeInterplanetario(objViaje.ViajeInterplanetario); // Ahora ejecutamos los delegados que harán los viajes viajeAnakin("Anakin", temporizador); viajeObiWan("ObiWan", temporizador); // Hacemos una pequeña pausa y luego mostramos el mensaje de fin de viajes System.Threading.Thread.Sleep(500); Console.WriteLine(); Console.WriteLine("=> Finalizó las llamadas a los viajes, a los {0} segundos.", temporizador.Elapsed.TotalSeconds.ToString()); Console.WriteLine(); Console.ReadLine(); } } public class Viaje { public int ViajeInterplanetario(string NombreViajero, Stopwatch temporizador) { Console.WriteLine(); Console.WriteLine("Inicio de viaje realizado por {0}.", NombreViajero); System.Threading.Thread.Sleep(5000); Console.WriteLine("Fin de viaje realizado por {0}, a los {1} segundos.", NombreViajero, temporizador.Elapsed.TotalSeconds.ToString()); return 0; } } }
Lo que hacemos en el código es crear la función ViajeInterplanetario que representará los viajes que harán los personajes.
Además, se crean 2 delegados que suscriben esta función y se llaman de forma síncrona (líneas 24 y 25):
// Ahora ejecutamos los delegados que harán los viajes (llamadas síncronas) viajeAnakin("Anakin", temporizador); viajeObiWan("ObiWan", temporizador);
También tenemos un objeto de apoyo llamado temporizador, que nos sirve para calcular el tiempo que demoran las llamadas y el programa principal.
Si ejecutamos el proyecto tenemos la siguiente salida:
Puedes notar que la ejecución es lineal: se llama al primer viaje, cuando éste termina se llama al segundo viaje y cuando éste segundo termina se muestra el mensaje de fin de llamadas de viajes. Esto sucede porque estamos llamando a los delegados de forma síncrona, que es casi lo mismo que llamar directamente a la función. El tiempo total de ejecución de todo el programa es un poco más de 10 segundos.
Llamada asíncrona a un delegado
Para probar el siguiente concepto haremos un cambio en la llamada a los delegados para que se ejecuten de forma asíncrona. Cambiamos las líneas 24 y 25 por llamadas asíncronas a los delegados:
// Ahora ejecutamos los delegados que harán los viajes (llamadas asíncronas) viajeAnakin.BeginInvoke("Anakin", temporizador, null, null); viajeObiWan.BeginInvoke("ObiWan", temporizador, null, null);
Si ahora ejecutamos el programa, tendremos lo siguiente:
Como puedes ver ahora la salida de la ejecución es diferente. El programa principal hace las llamadas (asíncronas) a los delegados y sigue su ejecución sin esperar a que estas llamadas terminen. Internamente la ejecución de dichas llamadas sucede en un hilo diferente al hilo del programa principal. Este manejo de hilos es interno, de forma transparente para nosotros.
Primero se muestran los mensajes de las llamadas a los viajes, luego se muestra el mensaje de fin de llamadas, para finalmente mostrar el mensaje de fin de viaje de cada llamada, que se ejecutaron de forma asíncrona.
Esto hace que al final el tiempo de ejecución de este programa, con la variante de las llamadas asíncronas, sea de aproximadamente 5 segundos, que es prácticamente la mitad del tiempo comparándolo con el tiempo de ejecución con llamadas síncronas del ejemplo anterior. Esto se explica porque los viajes se ejecutaron en «paralelo» (lo pongo en comillas porque hay toda una teoría sobre los procesos en paralelo, que espero abordarlo en un artículo posterior).
Conclusiones
Ya puedes imaginar el potencial de esta técnica, cómo implementar este tipo de llamadas para algunos procesos que demanden muchos recursos y poder reducir los tiempos de ejecución dividiendo las tareas a través de llamadas asíncronas. Pero no siempre es así: funciona bien para tareas que implican muchos cálculos (uso intensivo del procesador), pero para el manejo de archivos (por ejemplo) las ejecuciones asíncronas podrían demorar igual o más que si ejecutáramos las tareas de forma secuencial.
Como reflexión final quiero decir que si bien es cierto que para entender el concepto de programación asíncrona se ha hablado bastante de hilos de ejecución, hay que tener en claro que no son lo mismo. Los hilos son una herramienta que nos sirve para implementar la asincronía en nuestros aplicativos.
Espero que este artículo sea útil para entender la programación asíncrona. En los siguientes artículos de esta serie explicaré otras formas que existen para su implementación, tratando de seguir de forma cronológica la evolución de este tema en las versiones del .NET Framework.