viernes, 17 de agosto de 2012

C# Multithreading, Ejemplo IAsyncResult Pattern - BeginInvoke/EndInvoke

Hace años los sistemas operativos sólo soportaban un hilo de ejecución, en el momento en que evolucionaron hacía el Multithreading se abrió una vía de posibilidades muy interesante a los desarrolladores de cara a optimizar sus aplicaciones.

Existen varias posibilidades en C# para hacer que nuestras aplicaciones sean Multithreading.
  • Usar el espacio de nombres System.Threading: Tal vez esta sea la técnica más compleja ya que requiere conocer los conceptos STAT/MTAT para asegurar una gestión "ThreadSafe" de los hilos y entender como actua el contexto de sincronización para intercambiar información entre hilos.

  • IAsyncResult Pattern: Es un patrón que a partir de delegados permite invocar un método en segundo plano y recibir información cuando este termine su ejecución. A priori la gestión de los Threads resulta más sencilla que en el caso anterior. Veremos un ejemplo en este artículo.

  • BackGroundWorker: Microsoft recomienda el uso de este componente en nuestras aplicaciones Windows Forms siempre y cuando nuestros procesos necesiten interactuar con controles de la UI.

En este ejemplo veremos IAsyncResult Pattern, más adelante intentaremos publicar ejemplos sobre una gestión Multi-hilo con el resto de técnicas mencionadas en la sección Multithreading de areaTic.

Los métodos BeginInvoke() y EndInvoke() que se usan en el ejemplo, están disponibles vía CLR (Common Language Runtime) en todos los objetos que se usen en el espacio de nombres Windows.Forms. Tened en cuenta que si estamos programando en C# desde un ámbito diferente a Visual Studio es posible que IntelliSense del IDE no muestre referencia de estos métodos.

Vamos ya al ejemplo, crearemos un proyecto de tipo Aplicación de Consola desde Visual Studio en el que por defecto se incluirá la clase Program.cs con el método Main().

A continuación añadimos una clase Proceso.cs al proyecto la cual contendrá tres funciones que simularán realizar acciones que duran un tiempo determinado en cada caso.

namespace ThreadSamples
{
    public delegate string ProcesoNegocio(string pEntrada);

    public class Proceso
    {
        public string ProcesoNegocio1(string pEntrada)
        {
            //dormimos ejecución durante 7 segundos 
            //simulando una acción que tarda ese tiempo.
            System.Threading.Thread.Sleep(7000);
            //devolvemos fecha fin proceso concatenada a la fecha de entrada
            return string.Format("Fin Proceso 1: {0}-{1}", pEntrada, DateTime.Now.ToString());
        }

        public string ProcesoNegocio2(string pEntrada)
        {
            //dormimos ejecución durante 10 segundos 
            //simulando una acción que tarda ese tiempo.
            System.Threading.Thread.Sleep(10000);
            //devolvemos fecha fin proceso concatenada a la fecha de entrada
            return string.Format("Fin Proceso 2: {0}-{1}", pEntrada, DateTime.Now.ToString());
        }

        public string ProcesoNegocio3(string pEntrada)
        {
            //dormimos ejecución durante 3 segundos 
            //simulando una acción que tarda ese tiempo.
            System.Threading.Thread.Sleep(3000);
            //devolvemos fecha fin proceso concatenada a la fecha de entrada
            return string.Format("Fin Proceso 3: {0}-{1}", pEntrada, DateTime.Now.ToString());
        }
    }
}
Necesitaremos un delegado para cada método que tengamos intención de invocar asíncronamente. En este caso coincide el tipo de parámetro de entrada y salida de los 3 métodos por lo que nos valdría si creamos un sólo delegado.
namespace ThreadSamples
{
    public delegate string ProcesoNegocio(string pEntrada);
}

Crearemos también una clase ThreadSample.cs que nos facilitará la gestión del asíncrono y evitará que tengamos que programar en la clase principal Main.cs.



A continuación veremos como aplicar el patrón en la clase ThreadSample.cs, es importante fijarnos en el método IniciarProcesos y EndProceso.
  • IniciarProcesos: Se encarga de invocar las 3 funciones en Threads diferentes.

  • EndProceso: Se encarga de recibir las respuestas a medida que terminen su ejecución e informa en la consola de la aplicación sobre el fin de cada proceso.

En el ejemplo nos disponemos a ejecutar los 3 métodos desde la clase Main() pero abriendo un hilo diferente para cada método. Sin gestión Multithreading nuestra aplicación esperaría la respuesta del primer método para invocar al segundo y así sucesivamente. En este caso veréis que al implementar el patrón IAsyncResult cada método se invoca en un Thread diferente y al finalizar la ejecución cada Thread se comunica con el hilo principal para avisar que ha terminado la ejecución y es en este momento donde podemos recuperar los parámetros de salida de la función que hemos llamado asíncronamente.

A continuación veremos la implementación de la clase ThreadSample y como se usan los métodos BeginInvoke() y EndInvoke().

namespace ThreadSamples
{
    public class ThreadSample
    {
        ProcesoNegocio mProceso1;
        ProcesoNegocio mProceso2;
        ProcesoNegocio mProceso3;

        public ThreadSample()
        {
            //clase que contiene las 3 funciones que invocaremos
            Proceso Procesos = new Proceso();

            //definimos un delegado para gestionar cada una de las 3 llamadas asíncronas
            mProceso1 = new ProcesoNegocio(Procesos.ProcesoNegocio1);
            mProceso2 = new ProcesoNegocio(Procesos.ProcesoNegocio2);
            mProceso3 = new ProcesoNegocio(Procesos.ProcesoNegocio3);
        }

        public void IniciarProcesos()
        {
            //definimos un callBackObject que apunte al método EndProceso.
            AsyncCallback callback = new AsyncCallback(EndProceso);

            mProceso1.BeginInvoke(DateTime.Now.ToString(), callback, null);
            mProceso2.BeginInvoke(DateTime.Now.ToString(), callback, null);
            mProceso3.BeginInvoke(DateTime.Now.ToString(), callback, null);

            Console.Read();
        }

        public void EndProceso(IAsyncResult pReturn) 
        {
            string sReturn = "";
            //"casteamos" el objeto para tener un poco más de información y saber el método original del cual procede el CallBack
            //debido a que hemos "aprovechado" el mismo delegado para EndProceso.
            System.Runtime.Remoting.Messaging.AsyncResult pExtendedReturn = (System.Runtime.Remoting.Messaging.AsyncResult)pReturn;
            System.Delegate delegateInfo = (System.Delegate)pExtendedReturn.AsyncDelegate;
            //en función del método original llamamos al EndInvoke del delegado correspondiente para obtener la respuesta.
            switch (delegateInfo.Method.ToString()) 
            {
                case "System.String ProcesoNegocio1(System.String)":
                    sReturn = mProceso1.EndInvoke(pReturn);
                    break;

                case "System.String ProcesoNegocio2(System.String)":
                    sReturn = mProceso2.EndInvoke(pReturn);
                    break;

                case "System.String ProcesoNegocio3(System.String)":
                    sReturn = mProceso3.EndInvoke(pReturn);
                    break;
            }

            Console.WriteLine(sReturn);
        }
    }
}
En el constructor de la clase declaramos tres instancias del delegado ProcesoNegocio que apunten a los 3 métodos públicos de la clase Proceso.

El método IniciarProcesos() se encarga de definir un delegado de respuesta y hacer la llamada asíncrona a los 3 métodos. BeginInvoke espera tantos parámetros de entrada como hayan definidos en el delegado, en este caso tenemos tan solo un paramétro string de entrada. A parte de tantos parámetros de entrada como hayan definidos en el delegado, BeginInvoke permite pasarle un CallBackObject con firma void MethodName(IAsyncResult), a través del cual podemos indicarle el método de respuesta que ha de invocar una vez finalice la ejecución del proceso.

En cuanto a EndProceso, su función reside en llamar a delegado.EndInvoke() pasando el IAsyncResult que recibimos como entrada del método y ya veréis que el mismo método EndInvoke devuelve un tipo de datos igual que el que hayamos definido en nuestro delegado original.

Por último desde la clase Main() instanciamos ThreadSample y llamamos a IniciarProcesos(), el resultado será el siguiente:



Se puede ver en la imagen que nuestra aplicación no espera a finalizar proceso1 para ejecutar proceso2 si no que ejecuta cada uno en un Thread diferente y posteriormente gestiona las respuestas desde el hilo principal.

Resumiendo, es importante entender a nivel conceptual la secuencia de pasos que hemos seguido para implementar el patrón:
  • Instanciamos delegado D

  • Llamamos a D.BeginInvoke(Parametros Entrada + CallBackObject).

  • D ejecuta el método asociado y al finalizar llamará automaticamente a CallBackObject (método delegado respuesta).

  • Llamamos a D.EndInvoke(IAsyncResult) pasándole el IAsyncResult para recibir de vuelta los parámetros de salida del método que hemos invocado asíncronamente.
Gracias a esta técnica podemos agilizar procesos aprovechando toda la potencia del CPU que ejecuta la aplicación y evitar saturar el hilo principal con procesos pesados de negocio sin demasiada dificultad, sólo requiere dominar el uso de delegados en C#.

En caso que os dispongáis a implementar una gestión multi-hilo en una aplicación de tipo Windows.Forms / WPF y los hilos secundarios necesiten interactuar con elementos de la UI, aseguraos previamente que conocéis los requisitos para implementar una gestión ThreadSafe en Windows y así evitar problemas como el que se mencionaba en este artículo areaTIC: Error STATThreadAttribute

Espero os haya resultado útil el post, no olvidéis consultar el archivo de areaTIC tal vez hayan otros artículos que puedan interesarte!


2 comentarios:

JUNIOR FLOWER dijo...

Hola, está bien explicado, pero tengo una duda... En mi caso cómo puedo implementar el Thread si actualmente tengo en el evento clic de un botón más o menos la siguiente estructura:

Evento_Clic ...
{
datatable.colum["ColumnX"].setOrdinail(0);
datatable.colum["ColumnX"].setOrdinail(1);
datatable.colum["ColumnX"].setOrdinail(2);
.
datatable.colum["ColumnX"].setOrdinail(59);

for (int k = 0; datagridview1.rows.count -1; k++)
{
avanza++;
for (int q=datagriview1.rows.count-1;q >=0;q--)
{ ----
----
-----un monton de condiciones---
}
}
for ( ------)
{
for (-------)
{
}
}

for ( ------)
{
for (-------)
{
//varias condiciones
}
}
}

Cómo le podría hacer para que todos esos procesos que ejecuto en el evento clic del botón se ejecute en segundo plano ya que este proceso tarda aproximadamente 10 minutos, y cuando lo ejecuto se pone mi aplicación como “no responde” pero sin embargo se está ejecutando.
Saludos!

Carlos Cañizares dijo...

buenas, crearía una función privada donde ubicaría todo el código que realizas dentro del evento click. Luego un delegate con la firma del método que crees y lo invocaría dentro del click tal y como hacemos con ProcesoNegocio en el artículo. Si aún necesitas ayuda escríbeme a c.canizaresestevez@gmail.com que te echo un cable encantado.

Publicar un comentario