Programación Asíncrona | Trabajando con Delegados Asincronos C#

Se ha venido hablando mucho en los últimos años sobre la programación Asíncrona, ya que este esquema de programación puede mejorar mucho nuestros programas claro está si lo aplicamos correctamente.

En ocasiones nuestros programas nos dan la notificación de que el programa no responde o se congela  cuando el usuario interactúa con ellos. Esto ocurre porque hay algunas tareas pesadas que se están ejecutando detrás, y como sólo hay un hilo de ejecución,  entonces no le queda otra opción  que esperar a que termine dicha tarea para estar nuevamente disponible. Mejorar este tipo de comportamientos les da un toque de profesionalidad a nuestro trabajo algo  que siempre es importante.

En esta entrada veremos cómo aplicar la programación asíncrona usando delegados asíncronos, en otra ocasión hace tiempo escribí una entra sobre los delegados la puede leer en el siguiente enlace aquí y otro aquí.

Si con los enlaces no basta para comprender, a continuación haremos un pequeño ejemplo de cómo funcionan los delegados.

¿Qué es un Delegado?

Un delegado es la implementación de .NET de los punteros a funciones.

Ahora vamos al ejemplo “suuuuper” sencillo de un delegado.

Primero declaramos un delegado:

  delegate int delegadoProducto(int a, int b);

A continuación creamos una función con la misma estructura del delegado:

public class DelegadoProducto
    {
        public int Producto(int x, int y)
        {
            Console.WriteLine();
            Console.WriteLine("DelegadoProducto.Producto: Calculando el producto de {0} por {1}", x.ToString(), y.ToString());
            Console.WriteLine();
            return x * y;
        }
    }

Ahora instanciamos un objeto de nuestro delegado:

  public static void Main(string[] args)
        {
            // Instanciamos un delegado
            var objDelegado = new DelegadoProducto();
 
            // 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(4, 4);
 
            // Imprimimos el resultado de la ejecución
            Console.WriteLine("Resultado del producto es: {0}", resultado.ToString());
            Console.ReadLine();
        }

Si se fijan Sólo se puede asignar funciones a objetos que tienen la misma estructura que su delegado.

Esta es la salida de nuestro programa en consola en mi caso terminal puesto que estoy usando Mac OS.

Aplicando la programación Asíncrona a nuestros delegados.

Vamos con un ejemplo un poco más elaborado para entender mejor el concepto de llamada síncrona.

Para este ejemplo voy a usar a dos de mis amigas  Naiomi y Maria. Ellas harán un viaje hacia la ciudad de NEW YORK que demora 15 segundos aproximadamente. ( es solo un ejemplo)

El codigo el siguiente:

sing System;
using System.Diagnostics;
 
namespace ProgramacionAsincrona.Delegados
{
    class Program
    {
        delegate int DelegadoViajeNYC(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.
            DelegadoViajeNYC viajeNaiomi = new DelegadoViajeNYC(objViaje.ViajeNYC);
 
            // Se crea el segundo delegado, que representará el segundo viaje.
            DelegadoViajeNYC viajeMaria = new DelegadoViajeNYC(objViaje.ViajeNYC);
 
            // Ahora ejecutamos los delegados que harán los viajes
            viajeNaiomi("Naiomi", temporizador);
            viajeMaria("Maria", temporizador);
 
            // Hacemos una pequeña pausa y luego mostramos el mensaje de fin de viajes
            System.Threading.Thread.Sleep(1500);
            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 ViajeNYC(string NombreViajera, Stopwatch temporizador)
        {
            Console.WriteLine();
            Console.WriteLine("Inicio de viaje realizado por {0}.", NombreViajera);
            System.Threading.Thread.Sleep(15000);
            Console.WriteLine("Fin de viaje realizado por {0}, a los {1} segundos.", NombreViajera, temporizador.Elapsed.TotalSeconds.ToString());
            return 0;
        }
    }
 
}

Recapitulando

Si eres observador puedes notar que la ejecución del programa 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 31 segundos.

Realizando llamada asíncrona a un delegado

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)
            viajeNaiomi.BeginInvoke("Naiomi", temporizador, null, null);
            viajeMaria.BeginInvoke("Maria", temporizador, null, null);

Si ejecutamos nuestro codigo el resultado seria el siguiente:

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.

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. Espero que la apliquen en sus próximos desarrollos.

 

 

 

 

Concurrencia C#

etSq-1La Concurrencia permite que distintos objetos actúen al mismo tiempo, usando diferentes hilos de control (un solo proceso).

Es decir: Es la propiedad que diferencia a los objetos entre estar activos o no.

Esta la usamos  cuando tenemos que manejar varias acciones diferentes al mismo tiempo, para ello  utilizamos procesos los cuales producen acciones dinámicas independientes dentro de un sistema.

Sabemos hay sistemas los cuales ejecutan en múltiples CPU permitiendo así hilos de control verdaderamente concurrentes, mientras que en los sistemas que ejecutan en un solo CPU sólo se puede conseguir una ilusión de hilos concurrentes de control, normalmente mediante algún algoritmo de tiempo compartido.

 

La mayoría de las veces al diseñar o crear una aplicación en lo último en que se solemos pensar es en si debe o no emplear múltiples hilos para su ejecución; ya que cuando se nos enseña un lenguaje de programación lo que se suele hacer es programar todo dentro del mismo programa principal (el famoso Main( )) sin preocuparnos por nada más !!! :p .

Con el Hardware moderno se debe explotar al máximo las características que este nos ofrece y para ello son las aplicaciones las que se deben encargar de exprimir esos recursos.

¿porqué  utilizar Hilos para ello?

Pues la idea es simple un, Hilo es una especie de proceso muy pequeñito que se va a encargar de ejecutar un mismo trozo de código cada vez que se le diga por tanto consume muy poca memoria RAM y forma parte de un proceso más grande que lo conocemos como programa principal. Básicamente la idea es que el Hilo le ayude al programa principal a hacer su trabajo; por tanto el programa principal debe delegar algunas actividades para que estas sean realizadas por el Hilo.

Explicación:

BackgroundWorker es una clase de ayuda en el espacio de nombres System.ComponentModel para la gestión de un subproceso de trabajo.  Proporciona las siguientes características:

Un modelo de cancelación de cooperación
La capacidad de actualizar de forma segura WPF o controles de Windows Forms cuando el trabajador se completa
Reenvío de excepciones al evento de finalización
Un protocolo para informar sobre el progreso
Una implementación de IComponent lo que le permite estar situado en el diseñador de Visual Studio

Ejemplo:

Explicacion:

  • Establecer la propiedad WorkerReportsProgress true.
  • Periódicamente llamar ReportProgress desde dentro del controlador de eventos DoWork con un valor de “porcentaje completado”, y, opcionalmente, un objeto de estado del usuario.
  • Controlar el evento ProgressChanged, consultar la propiedad ProgressPercentage de su argumento evento.
  • Código en el controlador de eventos ProgressChanged es libre para interactuar con los controles de IU al igual que con RunWorkerCompleted. Esto es por lo general en el que se actualizará una barra de progreso.

Para añadir soporte para la cancelación:

  • Establecer la propiedad WorkerSupportsCancellation true.
  • Compruebe periódicamente la propiedad CancellationPending desde dentro del controlador de eventos DoWork. Si es verdad, ajuste del argumento de evento Cancelar propiedad en true, y volver.
  •  Llamar CancelAsync para solicitar la cancelación.
using System;
using System.Threading;
using System.ComponentModel;

namespace ConsoleApplication1
{
    class Program
    {

        static BackgroundWorker _bw;

        static void Main()
        {
            _bw = new BackgroundWorker
            {
                WorkerReportsProgress = true,
                WorkerSupportsCancellation = true
            };
            _bw.DoWork += bw_DoWork;
            _bw.ProgressChanged += bw_ProgressChanged;
            _bw.RunWorkerCompleted += bw_RunWorkerCompleted;

            _bw.RunWorkerAsync("Hola Programador");

            Console.WriteLine("Presiona enter en los proximo 5 segundos para  cancelar");
            Console.ReadLine();
            if (_bw.IsBusy) _bw.CancelAsync();
            Console.ReadLine();
        }

        static void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            for (int i = 0; i <= 100; i += 20)
            {
                if (_bw.CancellationPending) { e.Cancel = true; return; }
                _bw.ReportProgress(i);
                Thread.Sleep(1000);      // Solo por demo...no ira a dormir
            }                         

            e.Result = 123;    //  RunWorkerCompleto
        }

        static void bw_RunWorkerCompleted(object sender,
                                           RunWorkerCompletedEventArgs e)
        {
            if (e.Cancelled)
                Console.WriteLine("Has  cancelado!");
            else if (e.Error != null)
                Console.WriteLine("Error: " + e.Error.ToString());
            else
                Console.WriteLine("Completo: " + e.Result);      // desde DoWork
        }

        static void bw_ProgressChanged(object sender,
                                        ProgressChangedEventArgs e)
        {
            Console.WriteLine("Cargado " + e.ProgressPercentage + "%");
        }
    }
}

El código fuente está disponible en GITHUB clic aquí para descargarlo descarguen la carpeta Concurrencia.

Esto es todo por el momento alguna duda, comentario o sugerencia no duden en escribirme.