viernes, 13 de junio de 2014

Receta No. 4-1 en C#: Ejecución de un Método sobre un Pool de Threads

Tabla de Contenido

0. Introducción
1. Problema
2. Solución
3. Discusión de la Solución
3.1 ¿Qué es un pool de threads?
3.2 Clase ThreadPool
3.2.1 Método QueueUserWorkItem
4. Práctica: Código Fuente C#
5. Conclusiones
6. Glosario
7. Literatura & Enlaces

0. Introducción

Con esta receta empezamos la serie de threads, procesos, y sincronización. Aprenderemos que los sistemas operativos modernos (como Microsoft Windows) cuentan con la poderosa maquinaria que facilita la ejecución de múltiples programas en paralelo. Los programas son análogos a los procesos, y un proceso puede estar compuesto por uno o varios threads (construcción básica para permitir la ejecución de bloques de código). Las demostraciones creadas en las recetas de las tres series desarrolladas hasta este punto, todas incluían una sección de código cliente encerrada en el método Main que es el thread de ejecución principal que ejecuta instrucción por instrucción. El tiempo total de ejecución se mide como la sumatoria de los tiempos de cada instrucción (o por cada bloqueo). En contraste, un método Main compuesto por varios hilos de ejecución, el tiempo  puede ser menor gracias a la porción tiempo (time slice) que el sistema operativo asigna a cada hilo y produce el efecto de ejecución al unísono (simultánea); lo que se convierte en una ganancia en el desempeño de la aplicación.

Ahora sí: con esta receta vamos a prender cómo ejecutar un método sobre un pool de threads. Conoceremos una de los primeros artefactos de concurrencia que posee Microsoft .NET Framework en su biblioteca base de clases: ThreadPool, y varios de sus métodos, delegados con firmas estándar para métodos que requieren entrar al pool de threads. Introduciré tres ejemplos demostrativos de las capacidades de la clase ThreadPool sobre los que podamos comprender su utilidad y ventajas en la programación multihilo.

1. Problema

Requerimos modernizar varios de los programas que aún funcionan en ambientes de ejecución legados. A pesar de la longevidad de estas aplicaciones, aún permiten incorporar mejoras de su rendimiento. El arquitecto de software nos ha encargado actualizar estas aplicaciones para permitir la ejecución en paralelo de varios de los artefactos integrales de la solución (miembros de clases, métodos utilitarios, etc.).

2. Solución

El equipo de programación conoce el método idóneo para permitir la ejecución de bloques de código dentro de lo que se conoce pool de threads. Sobre el pool de threads podemos colocar tareas a través del uso del método static QueueUserWorkItem. de la clase ThreadPool (N:System.Threading) Este método posee dos versiones sobrecargadas. La más simple permite especificar un delegado del tipo WaitCallback (System.Threading) como parámetro. Este delegado puede envolver a un método que será ejecutado desde el pool de threads.

3. Discusión de la Solución

3.1 ¿Qué es un pool de threads?

Un pool de threads comprende una de las formas de alcanzar el efecto de producido por la programación basada en multitheads (existencia paralela de múltiples threads en un único proceso). Un thread corresponde con una unidad de ejecución de código: acceso archivos, cálculo de una expresión matemática, ejecución de sentencias de la lógica de un bloque de construcción, descarga o subida de un flujo de bytes o caracteres, entre muchos más.

Un pool de threads comprende una cola en la que se agregan los threads para la ejecución en paralelo de los mismos.

3.2 Clase TheadPool

La clase ThreadPool [3] provee el receptáculo o pool de threads que permite la ejecución de tareas, establecimiento de unidades de trabajo para ejecución en paralelo, procesamiento asincrónico de entrada y salida, control sobre la ejecución de los threads (e.g., espera para la terminación de los threads sobre el pool).

En [3] podemos encontrar su firma:

public static class ThreadPool


A notar que se trata de una clase estática, por lo tanto cualquier intento de instanciar de este tipo nos generará un error en tiempo compilación.


Por otro lado, podemos destacar algunos escenarios de uso de este tipo de facilidad para la ejecución en paralelo de múltiples threads:
  • Cuando recurrimos al uso de una instancia de Task (cfr. Expresiones Lambda y Asincronismo) para la ejecución de tareas (eventos, métodos, etc.) asincrónicos.
  • Los temporizadores (timers) asincrónicos se localizan en un pool de threads para controlar la generación de eventos (e.g., tic).
  • Gestión de la espera sobre unidades de trabajo hasta completar su(s) tarea(s), y responder con eventos una vez este evento haya ocurrido.
Hay que destacar que un pool de threads se ejecuta en segundo plano (background), y a esto vale agregar que, por ejemplo, si hemos creado el pool de threads sobre un thread de primer plano, una vez este último termine los threads de segundo plano se interrumpirán. En la nota en [3] mostrada en la Figura 1 destacan con más detalle este fenónomo.
Nota acerca del funcionamiento jerárquico de la clase ThreadPool
Figura 1. Nota acerca del funcionamiento jerárquico de la clase ThreadPool.

Conozcamos con detalle el miembro de ThreadPool que nos permite agregar métodos (o tareas) para ejecución sobre un pool de threads.

3.2.1 Método QueueUserWorkItem

El método estático sobrecargado QueueUserWorkItem permite agregar a la cola (estructura interna de ThreadPool que representa el receptáculo de las tareas agregadas y ser ejecutadas) métodos para ser ejecutados por uno de los hilos de ejecución disponibles en el pool. A continuación la lista sobrecargada de este método:
Lista sobrecargada del método QueueUserWorkItem
Figura 2. Lista sobrecargada del método QueueUserWorkItem.
Notemos que para ambas versiones el primer parámetro corresponde con el delegado WaitCallback (ubicado en el namespace System.Threading) [2]. Esta su firma:

public delegate void WaitCallback(Object state)

Cualquier método que pasemos como instancia a este delegado debe poseer la siguiente firma:
  • Parámetro: object
  • Tipo de retorno: void
Sin embargo, la segunda versión sobrecargada de QueueUserWorkItem [9] , podemos especificar un contenedor de información útil para el método invocado indirectamente por el delegado. Esta es la firma:
public static bool QueueUserWorkItem(WaitCallback callBack, Object state)

Creamos un ejemplo para versión sobrecargada de este método:

Ejemplo de uso de QueueUserWorkItem(WaitCallback):

Archivo de código fuente UsoQueueUserWorkItemV1.cs [enlace alternativo]:
En la línea 11 invocamos al método QueueUserWorkItem y le pasamos como argumento una instancia del delegado WaitCallback, el cual encapsula al método ProcesoThread (líneas 25-28). Cuando este método se haya ejecutado por uno de los threads del pool mostrará en la salida estándar el siguiente mensaje:

Mensaje desde el pool de threads.


En el método Main (thread que corre en primer plano), específicamente en la línea 18, detenemos el thread principal por un segundo para darle la oportunidad al pool de threads ejecutar el método ProcesoThread; de lo contrario la aplicación terminaría sin alcanzar ejecutarlo.

Compilación:


  1. csc /target:exe UsoQueueUserWorkItemV1.cs

Ejecución assembly:


  1. .\UsoQueueUserWorkItemV1.exe

Resultado:
Ejecución pool de threads
Figura 3. Ejecución pool de threads.


Ejemplo de uso de QueueUserWorkItem(WaitCallback, Object):

Archivo de código fuente UsoQueueUserWorkItemV2.cs [enlace alternativo]:

En las líneas 8-18 se define la clase InfoTarea que será la clase contenedora de la información que será pasada en el segundo argumento del método QueueUserWorkItem; esta información será usada por el método ProcesoThread cuando sea invocada por uno de los threads del pool.


Notemos como en la línea 26 se crea una instancia de InfoTarea con el constructor que inicializa sus campos. Sobre la línea 29 se invoca al método QueueUserWorkItem con los argumentos:
  • WaitCallback: encapsula al método ProcesoThread.
  • Object: instancia de InfoTarea -infoTarea-
Compilación:


  1. csc /target:exe UsoQueueUserWorkItemV2.cs

Ejecución assembly:


  1. .\UsoQueueUserWorkItemV2.exe

Resultado:
Ejecución pool de threads
Figura 4. Ejecución pool de threads con el método QueueUserWorkItem(WaitCallback, Object).

4. Práctica: Código Fuente C#

Algunos de los artefactos incluidos en esta sección práctica de la receta, pueden resultar sorpresivos, sin embargo incluiré una descripción de uso en los comentarios enseguida de la exposición del código fuente C#.

Archivo de código fuente FibonacciMultithread.cs [enlace alternativo]:
En las líneas 6-67 se declara la clase FibonacciMultithread que incluye las siguientes declaraciones (solo las más sobresalientes):
  • Línea 13: Declaración instancia de ManualResetEvent para la notificación de eventos sobre el pool de threads.
  • Líneas 39-53: Declaración del método ControlSegundoPlano para controlar los métodos que serán ejecutados por el pool de threads.
En el código cliente (líneas 71-112) se crean los siguientes elementos de programa:
  • Línea 74: constante con el número de series Fibonacci a calcular.
  • Línea 79: arreglo con objetos ManualResetEvent para cada instancia de FibonacciMultithread.
  • Línea 82: arreglo con el número de instancias de FibonacciMultithread sobre las que se realizará el cálculo de la serie.
  • Línea 86: instancia de Random para la generación de números pseudoaleatorios.
  • Líneas 90-99:
    • Línea 92: creación de instancia de ManualResetEvent para la instancia de FibonacciMultithread actual (línea 93).
    • Línea 98: agregación al pool de threads de la instancia de FibonacciMultithread recién creada.
Con WaitHandle.WaitAll(calculoCompletos) (línea 102) establecemos la espera de finalización de todos los thredas del pool.

Compilación:


  1. csc /target:exe FibonacciMultithread.cs

Ejecución asembly:


  1. .\FibonacciMultithread.exe

Prueba de ejecución:
Prueba de ejecución de FibonacciMultithread
Figura 5. Prueba de ejecución de FibonacciMultithread.


En la Figura 5 se muestra el proceso de compilación, y ejecución. En cuanto a la ejecución se realizan 5 pruebas diferentes para demostrar que los threads no tienen un orden específico de ejecución. Esta tarea es totalmente manejada por la CLR y el sistema operativo.

5. Conclusiones

En esta receta hemos aprendido a crear un pool de threads y ponder dentro de él métodos que serán ejecutados por automáticamente. Estas operaciones son sencillas gracias al grado de simplicidad que posee el lenguaje de programación C#, y por supuesto el propio Framework .NET. Aprendimos de las dos versiones de QueueUserWorkItem a través de su definición sintáctica, y en particular, en los ejemplos presentados en la sección 3.2.1.

6. Glosario

  • Asincrónico
  • CLR
  • Evento
  • Pool
  • Sincrónico
  • Temporizador

7. Literatura & Enlaces

[1]: Visual C# 2010 Recipes by Allen Jones and Adam Freeman. Copyright 2010 Allen Jones and Adam Freeman, 978-1-4302-2525-6.
[2]: WaitCallback Delegate (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.waitcallback(v=vs.110).aspx
[3]: ThreadPool Class (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.threadpool(v=vs.110).aspx
[4]: Thread Pooling (C# and Visual Basic) - http://msdn.microsoft.com/en-us/library/h4732ks0.aspx
[5]: ManualResetEvent Class (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.manualresetevent.aspx
[6]: WaitHandle Class (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.waithandle(v=vs.110).aspx
[7]: Thread (computing) - Wikipedia, the free encyclopedia - http://en.wikipedia.org/wiki/Multithreading_(software)#Multithreading
[8]: ThreadPool.QueueUserWorkItem Method (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.threadpool.queueuserworkitem(v=vs.110).aspx
[9]: ThreadPool.QueueUserWorkItem Method (WaitCallback, Object) (System.Threading) - http://msdn.microsoft.com/en-us/library/4yd16hza(v=vs.110).aspx


J

No hay comentarios:

Publicar un comentario

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.