martes, 7 de julio de 2009

¿System.Threading? ¡Cómo y Cuándo! (C#)

La programación Multi-Threading es uno de los aspectos peores manejados entre los programadores, por eso antes de comenzar a utilizarlos, debemos saber en qué consiste.



¿Qué es un hilo de ejecución?

Un Hilo de ejecución o Thread es el contexto en donde se está ejecutando una porción de código, es decir, desde que comienza un programa éste fluye a través de un Hilo de ejecución (de ahora en adelante Thread). Antes de que los sistemas operativos soportaran Multi-Threading solamente un Thread era el que llevaba el flujo de la aplicación. Ahora con las nuevas tecnologías y lo avanzados que están los sistemas operativos podemos crear nuevos Threads para desarrollar aplicaciones que las aprovechen y así mejorarlas en varios aspectos como el rendimiento o simplemente mejorar la interfaz de usuario.


Ok ya se que es Multi-Threading, pero ¿Cuándo debo utilizarlo?

Existen varias ocasiones en las que es ideal el uso de un Thread o varios en conjunto para realizar una tarea. Por ejemplo: 

Caso 1:

En un formulario tienes una consulta que se tarda aproximadamente 1 minuto, la consulta se ejecuta en el evento Click de un botón llamado button1 (jeje) , el usuario al hacer click y lanzar el evento va a tener que esperar que el proceso termine, mientras ésto ocurre el formulario se deshabilita ya que el Thread que ejecuta el proceso es el mismo que dibuja la ventana, por lo cual aparece el famoso "(No Responde)" en el título.

- Solución: Lanzar el proceso en otro Thread y deshabilitar el botón hasta que termine. Mientras se ejecuta avisar al usuario, ya sea el progreso o simplemente el estado.

Caso 2:

Tienes dos procesos (A y B) que se tienen que ejecutar al mismo tiempo, y no tienen nada que ver uno con otro y normalmente ejecutarías A y luego B para luego continuar el flujo de la aplicación, pero ¿Qué pasaría si A se tarda 30 segundos y B 45 segundos?

- Solución: utilizar dos hilos paralelos para la ejecución y así tomar ventaja de los Procesadores nuevos multi-núcleo.


Existen otros casos, de los que estaremos hablando en próximos post!


Ahora por fin, ¿Cómo lo hago?

Antes de comenzar necesitamos definir un último concepto, llamado Delegado. Un delegado es un tipo de dato especial el cual tiene como función "apuntar" a un método cualquiera. Para que ésto ocurra el delegado debe de estar declarado de forma tal que coincida con la firma del método (Entiéndase firma por los parámetros).

Para todos los ejemplos se usará el siguiente método: void DoProcess(string text).

Cómo uso un delegado:

Primero creamos un delegado que coincida con la firma del método:

delegate void DoProcessDelegate(string text);

luego instanciamos:

DoProcessDelegate delegate = new DoProcessDelegate(DoProcess);

y ejecutamos:

string a = delegate("hola mundo");

Si! así de fácil!

Y ahora bien, existen varias vías para crear un proceso nuevo e iniciarlo:

Utilizando un Delegado

Instanciamos y apuntamos hacia el método:

DoProcessDelegate delegate = new DoProcessDelegate(DoProcess);

Llamamos al método BeginInvoke del delegado para comenzar la ejecución asíncrona donde nos pide los parámetros del delegados más dos, cuales son: AsyncCallback y un object.

El AsyncCallback hace referencia al método que se llama cuando se completa el proceso asíncrono.

El object lo recibe el método que apunta el AsyncCallback.

Ahora tenemos otro método llamado void ProcessEnded(IAsyncResult obj) y pasamos "prueba" como token, aunque debería ser un valor diferente en casos de que se ejecute varias veces la llamada a BeginInvoke En éste caso podemos pasar un Guid: Guid.NewGuid().

delegate.BeginInvoke("Hola Mundo", new AsyncCallback(ProcessEnded), "prueba");

Ahora el  método DoProcess se ejecuta de forma asíncrona y luego el ProcessEnded cuando éste último termine y le llega como parámetro "prueba".

Y ¿Qué pasa si nuestro DoProcess retorna algún valor? pues simplemente en el método ProcessEnded llamamos a delegate.EndInvoke(obj). El obj es el parámetro de ProcessEnded.


Utilizando la clase Thread

Otra forma de ejecutar un método asíncrono es usando la clase Thread. Ésta clase es la base de la ejecución de los Threads. Su funcionamiento es un poco más complicado. Ojo! su funcionamiento más no su llamado, es decir, mediante el uso de la clase Thread tenemos a la mano un conjunto de opciones y clases un poco más avanzadas sobre todo para el tema de la sincronización, de el cual les hablaré en otro post.

Básicamente la clase tiene dos constructores, uno pide un delegado ThreadStart y otro pide un delegado ParametrizedThreadStart, el primero es para apuntar hacia un método sin parámetros y el otro hacia uno con un parametro tipo object.

Ambos funcionan de la misma manera por lo que vamos a utilizar ThreadStart. Ahora tenemos un método nuevo void DeleteAllFiles();

Thread myThread = new Thread(new ThreadStar(DeleteAllFiles));

ahora simplemente para iniciar el thread solo hace falta llamar al método:

myThread.Start();

Éste tiene una sobrecarga de un parametro object para el ParametrizedThreadStart.


La clase ThreadPool 

Un pequeño ejemplo del uso de ThreadPool es el siguiente :

El método QueueUserWorkItem tiene un parámetro el cual es un delegado de tipo WaitCallback, que es la referencia al método que se va a ejecutar. También tiene una sobrecarga para pasar un object.  

public void main(string[] args)

{

        ThreadPool.QueueUserWorkItem(new WaitCallback(MyMethod));

         Thread.Sleep(5000);  //El Sleep "duerme" el hilo acual, sino hacemos ésto,

                                          //otra cosa u proceso el hilo del ThreadPool no

                                          //se ejecutará ya que el hilo principal se termina.

}

public void MyMethod(object obj)

{

       //Algún proceso

}


El componente BackgroundWorker

El BackgroundWorker es un componente que podemos arrastrar hasta el formulario, control de usuario o simplemente declararlo dentro de nuestro código. Ejmplo básico de su uso:

static void main(string[] args)

{
     BackgroundWorker worker = new BackgroundWorker();
     worker.DoWork+=
new DoWorkEventHandler(worker_DoWork);
     worker.RunWorkerAsync();
//aquí tenemos una sobre carga para pasarle

                                                 //un object al método worker_DoWork

}

static void worker_DoWork(object sender, DoWorkEventArgs e)

{

     // éste código se ejecuta asíncrono

     //en el parámetro DoWorkEventArgs encontramos el object que se envia desde RunWorkerAsync()

     //además de la posiblidad de cancelar en e.Cancel, y de enviar un valor de retorno por e.Result

}


Además de proveernos con funcionamiento asíncrono, el BackgroundWorker nos ofrece maneras para que nuestros métodos "reporten" el progreso del proceso, además de la capacidad de permitir la interrupción de manera fácil de nuestro Thread. El resultado de la operación que se envía a través de e.Result lo podemos encontrar fácilmente suscribiéndonos al evento RunWorkerCompleted del  BackgroundWorker.


Bueno ésto fue todo por ahora! Pronto publicaré más artículos cómo éste y de otros tipos también!..

Gracias por leer! y no se olviden de comentar :)


13 comentarios:

  1. hola que tal ante nada decirte que blog esta chidisimo ya lei varios de tus articulos, pero tengo un problema con un programa que estoy haciendo para mi resulta que leo un archivo mp3 y este lo comparo con otro archivo mp3 por medio del tag y me dice si son iguales pero resulta que donde esta mis archivos mp3 son 13000 mp3 y cada uno se compara con los 13000 osea que son mas o menos 169000000 comparaciones y me sale el error de contextswitchdeadlock me dice que la operacion tarda mas de 60 segundos y todo eso y me dice que devo utilizar MTAThreadAttribute para este error mi pregunta es si puedo utilizar la clase Thread en ves del MTAThreadAttribute para solucionar mi problema o en su defecto si me puedes ayudar a entender que es el MTAThreadAttribute te lo agradeceria mucho gracias por tu tiempo, y sigo insistiendo esta muy chido tu blog

    ResponderEliminar
  2. Para ayudarte más necesito más datos, pero bueno. Con lo que puedo ver te digo lo siguiente:

    Si usas MTAThreadAttribute ese mismo proceso NO puede abrir un formulario. Si tu aplicación no requiere formulario (GUI) entonces, MTA es la vía, ya que MTA hace que la comunicación entre subprocesos (threads) en tu aplicación sea más rápida.

    Cuando Es STA, Single Thread Apartment, entonces toda la comunicación entre los thread se hace mediante marshaling, por eso es que puede tardarse tanto.

    El MTA, MultiThreadAttribute hace que dentro de un apartment, algo asi como que tu proceso principal, puedan haber muchos subprocesos en donde toda la sincronización la harías tú mismo.

    Con respecto a contextswitchdeadlock, puede pasarte cuando consumes COM dentro de un entorno administrado, y el consumo de memoria se incrementa considerablemente, el cual es tu caso.

    Supongo que este error te esta dando cuando estás depurando la aplicación, es decir en modo debug, ya que el contextswitchdeadlock es una situación de deadlock cuando consumes COM y el MDA (managed debugging assistant) esta habilitado.

    Un saludo, espero pueda ayudarte.

    ResponderEliminar
  3. hola que tal gracias me ayudo bastante ya entendi por que no no podia utilizar el MTAThreadAttribute, de echo si utilizo windows Form para mi programa, esto lo Hago en en el modo debug ya lei por la web que si desavilito el contextswitchdeadlock mi programa deveria correr pero ya lo hice y si trabaja durante unos segundo despues simple mente deja de funcionar tengo que parar el programa.
    te explico brevemente como funciona mi programa haber si me puedes ayudar un poquito mas.

    En un evento Click de un boton ago que me seleccione un directorio con el FolderBrowserDialog(ej: c:\musica\) despues con un foreach Hago que busque solo mp3 encontrando el primero mp3 mando a una funcion(comparacion) la direccion del mp3(ej: c:\musica\musica.mp3) y el directorio que selecione (ej: c:\musica) despues empiesa a comparar el mp3 que envio con todos los que hay en la carpeta musica si son iguales me guarda el directorio en un archivo para que cuando termine los muestre en listbox y cuando seleccione 1 me diga cuantos cuanto encontro repetidos y m e los muestre en un listview.

    que me recomendarias hacer que devo utilizar para que no me salga el mensaje de (No Responde)y no se quede parado mi programa y se mas rapido mi programa .

    de ante mano un saludo y muchas gracias por tu tiempo.

    PD:
    aveces donde dice "Comentar como:" no se puede selecionar nada tengo que volver abrir la pagina.
    y por cierto espero el segundo articulo de desarrollo de juegos.
    abra alguan manera de poder poner un nick es que no tengo ninguna cuenta de las que aparecen en Comentar como.
    Atte.
    xEGAx

    ResponderEliminar
  4. Bueno, puedes correr ese método en otro proceso, cosa que haría que el No Responde no apareciera, pero requiere un poco más de programación, puedes ver como funciona en este mismo post, en el titulo "Utilizando la clase Thread".

    Con respecto a lo de Comentar como, eso es un problema de Blogger como tal, ya acá no puedo hacer nada. Aunque espero pronto montar mi propio dominio para mi blog, con mi propio motor del blog. :)

    Ahora ando bastante corto de tiempo, pero, pronto publicare el segundo artículo, y muchos más.

    Me avisas como te va con el proyecto!

    Un saludo!

    ResponderEliminar
  5. ok muchas gracias por tu tiempo y disculpa las molestias te avisare cuano termine mi programa agregando la "Utilizando la clase Thread"
    un saludo y gracias otra ves.
    atte.
    xEGAx

    ResponderEliminar
  6. hola que tal yo otra ves molestando ya le agregue Threads pero resulta que me sigue sin funcionar, por que se queda paralisado el programa, si hago una comparacion pequeña con unos 20 mp3 si me funciona el programa pero cuando hago unos 4000 mp3 trabja un rato y desdues ya no, el thread lo creo y le mando un valor tipo objeto algo asi mira:

    ParameterizedThreadStart Parametro = new ParameterizedThreadStart(Compara);
    Thread Proceso = new Thread(Parametro);
    Proceso.Start(Arc1);
    Proceso.Join();

    Arc1 es un archivo tipo objet donde envio un dato (string).
    esto lo hago desde un evento de un boton, (compara) es el nombre de la funcion donde comparo todos los mp3, y me dice si son iguales, ai mando a llamar otra funcion que me guarda los que son iguales, cuando lo ejecuto no aparece el error de no responde pero simplemente no lo puedo minimizar o maximizar de echo nada, le coloque un texbox antes de que cree el thread para que me diga que le esta enviando pero no lo muestra.
    a hora otra cosa, cuano ando comparando quise que de una ves me agregara en un listbox los repetido pero resulta que me dice que objet no se creo en ese hilo(Thread) abra alguan manera de decirle al hilo principal que los muestre.
    disculpa las molestias amigo.
    y gracias.
    atte.
    xEGAx

    ResponderEliminar
  7. A ver, intenta quitar esta linea y has la prueba de nuevo: Proceso.Join();

    ResponderEliminar
  8. jjejeje sip, ya hace lo que deveria hacer a hora resulta que lo puedo mover minimizar y maximizar pero todos los controles (botones, texbox, listview etc) no funcionan, preguntas :
    eso ocurre por que tengo poca memmoria(1gb) o es otro problema?
    ¿cuando se ejecuta el thread y hace todas las conparaciones y termina de comparar y regresa muere el Thread o lo tego que terminar yo?
    esto lo voy a plicar con Sockets y necesito hacer que un hilo me este leyendo el puerto de escucha todo el tiempo ¿como lo puedo hacer ?.
    gracias y mil disculpas por la molestia
    amigo.
    atte.
    xEGAx

    ResponderEliminar
  9. El subproceso termina cuando el método que esta corriendo en el nuevo Thread que creaste. Por lo que justo antes de terminar éste método tienes que usar un delegado invocado por el formulario para poder habilitar los controles que hayas deshabilitado.

    Con respecto al tema de sockets hay dos Post acá sobre Networking, te deberían funcionar. Un saludo!

    ResponderEliminar
  10. Te juego que es mi ultima duda y ya no te vuelvo a molestar.
    resulta que el thread lo tengo dentro de un foreach lo que estube leyendo por la red es que dentro del foreach me cre infinidad de hilos si asi por eso me salen un chingo de errores al como "el archivo no se puede leer por que esta siendo utilizado por otro proceso mi duda es el foreach lo tengo que poner dentro del thread y sobre lo ultimo que escribiste a que te refieres "El subproceso termina cuando el método que esta corriendo en el nuevo Thread que creaste. Por lo que justo antes de terminar éste método tienes que usar un delegado invocado por el formulario para poder habilitar los controles que hayas deshabilitado." es que soy medio burro y no te entendi con lo de metodo.
    y de verdad mil disculpas por quitarte tu tiempo y mil gracias.
    un saludo desde chiapas mexico.
    atte.
    xEGAx

    ResponderEliminar
  11. Claro, si creas un Thread en cada paso del foreach entonces estarás creando demasiados procesos, por eso es que te da tantos errores, el foreach va corriendo dentro del Thread.. Y cuando termine el foreach es que vas a avisar a que terminaste el proceso!

    ResponderEliminar
  12. muchisimas gracias y otra ves mil disculpas eres la leche de aca en adelante sigo yo.
    por cierto lo hice antes de que publicaras tu ultima respuesta y si me hace eso namas falta saber como termino mi proceso jajaja eso lo busco por la web un saludo y mil gracias.
    atte
    xEGAx

    ResponderEliminar
  13. Hola:
    Lo primeor gracias por la explicación, es sencilla y esclarecedora. Una pregunta:
    Estoy utilizando el BackgroundWorker, y quiero añadirle cierta funcionalidad extra, como por ejemplo SubProcessChanged (otro evento para subprocesos). El problema es que no sé como crear los eventos de forma que no me salte el InvalidOperationException.

    ResponderEliminar