miércoles, 9 de julio de 2014

Receta C# No. 4-7: Sincronizar la Ejecución de Múltiples Threads usando un Monitor

Tabla de Contenido

0. Introducción
1. Problema
2. Solución
3. Discusión de la Solución
3.1 Ejemplos de uso de la clase Monitor
3.1.1 Uso de métodos Enter y Exit
3.1.2 Uso de métodos Wait y Pulse
3.1.3 Sincronización de pool de threads
3.2 Consideraciones importantes de sincronización
4. Práctica: Código C#
5. Conclusiones
6. Glosario
7. Literatura & Enlaces

0. Introducción

En esta receta C# vamos a aprender a realizar una de las tareas más comunes cuando se trata de controlar el acceso a recursos y datos compartidos (e.g., conexiones de red, archivo) la cual consiste en la sincronización de la ejecución de múltiples threads. Esta técnica se logra a través de un monitor que monitoriza la ejecución de áreas críticas de código, evitando así la corrupción de los datos o inconsistencias en los resultados de un cálculo complejo. Para llevar esto a cabo usaremos la clase Monitor, la cual posee el conjunto de métodos estáticos necesarios para completar esta tarea de sincronización de manera simple y directa.

1. Problema

En las recetas anteriores no nos hemos preocupado por controlar o coordinar la ejecución de los diferentes threads en ejecución; de ahí que haya nacido la necesidad de encontrar un método o mecanismo para asegurar el acceso eficiente a recursos compartidos (e.g., conexión de red, archivo en disco).

2. Solución

Para coordinar la ejecución de múltiples threads es necesario sincronizar sus actividades de acceso a recursos compartidos. Uno de los enfoques práctico para lograr esto es a través de la clase Monitor (namespace System.Threading). En general, estos son los pasos a seguir para lograrlo:
  1. Identificación del objeto para mecanismo de control al recurso (objeto bloqueante) o datos compartidos.
  2. Usar los métodos estáticos Enter y Exit para bloquear el acceso y finalizar el bloqueo, respectivamente.

3. Discusión de la Solución

Para crear aplicaciones basadas en múltiples threads es necesario hacer que estos coordinen sus actividades de acceso a recursos y datos compartidos. A este mecanismo de coordinación de threads se le conoce comúnmente como sincronización.

La sincronización consiste en realizar las siguientes tareas [1]:
  • Asegurar que los diferentes threads en ejecución accedan uno tras otro (en serie) a los datos o los recursos compartidos para evitar la corrupción de estos.
  • Asegurar que los threads se ejecutan en el momento oportuno, y que además eviten la sobrecarga del sistema por permanecer tanto tiempo en espera.
Para esto, Microsoft .NET Framework cuenta la clase System.Threading.Monitor [2]. Con esta clase se logra que uno y solo un thread acceda a una región de código (está región de código puede comprender el acceso/modificación a un archivo, la conexión a un recurso de red [e.g., base de datos, servicio Web]). Precisamente el miembro estático de clase usado para la tarea anterior consiste en el método Enter [3].


A la región de código que mencionamos anteriormente, también se le conoce como región crítica. Esta región crítica debe ser bloqueada por una instancia de un tipo por referencia, inclusive el identificador de la instancia actual this. (En [1] recomiendan usar cualquier otra instancia como bloqueador en lugar de this.) Una vez que un thread alcanza esta área de código crítica, otro thread que intente hacer lo mismo pasará al estado WaitSleepJoin (cfr. Obtener el Estado de un Thread) y será agregado a la cola de espera hasta que la región se libere por el objeto bloqueador con la invocación del método Monitor.Exit [6]. Es recomendable encerrar la sección de código crítica en un bloque try y la invocación de Exit sobre el bloque finally. Así:

object locker = new object();

Monitor.Enter (locker);

try
{
// Conjunto de instrucciones sincrónicas
}
finally
{
Monitor.Exit (locker);
}


Con el objeto locker especificamos la instancia que se encargara de bloquear la región de código crítica. Observemos el uso del método Enter; a este método se le pasa como argumento la referencia que bloquea la región de código. En el bloque finally se invoca al método Exit para liberar la sección crítica.


Por otra parte, para situaciones de sincronización más complejas; como por ejemplo para la activación de determinados threads de la cola de espera y el control sobre cantidades ingentes de threads que podrían sobrecargar la unidad central de procesamiento (CPU);  la clase Monitor cuenta con los siguientes métodos:
  • Wait [7]: Se encarga de liberar el bloqueo y el thread en la cola de espera.
  • Pulse [8]: Notifica a uno de los threads en la cola de espera que el estado del objeto bloqueante (i.e., locker) ha cambiado su estado. El thread pasa a la cola de preparados (ready queue).
  • PulseAll [9]: Notifica a todos los threads en la cola de espera que el estado del objeto bloqueante ha cambiado su estado. Todos los threads pasan a la cola de preparados (ready queue).
Veamos varios ejemplos de uso de la clase Monitor.

3.1 Ejemplos de uso de la clase Monitor

3.1.1 Uso de métodos Enter y Exit

En este primer ejemplo comprenderemos cómo podemos sincronizar el acceso a un archivo usando los métodos Enter y Exit de la clase Monitor.

Archivo C# AccesoArchivoConMonitor.cs [enlace alternativo]:

Con el ciclo for en las líneas 41-46 creamos hasta 5 threads que van intentar acceder al archivo (nombre-threads.txtpara agregar una línea de texto con su nombre (línea 44). Por cada iteración del ciclo se inicia la ejecución (línea 45) del thread recién instanciado. En la línea 9 creamos una instancia del objeto bloqueante para la sincronización que se efectúa dentro del método EscritirArchivo.


Dentro del método EscritirArchivo (líneas 11-37) se empieza con una línea que pausa el thread actual durante 1000 milisegundos (1 segundo) (línea 13). Obtenemos el nombre del thread en la línea 15. (Este nos va a servir para agregar la entrada de texto sobre el archivo.) En plena línea 20 invocamos al método Enter y le pasamos el objeto bloqueante declarado en la línea 9. (A partir de aquí empieza la sección crítica.) En el bloque try se abre/crea el archivo nombres-threads.txt con la clase StreamWriter [9] y se agrega el nombre y número de thread al archivo (línea 25).


Continuando, en el bloque finally se invoca al método Exit para desbloquear o finalizar la sección crítica y dar paso al siguiente thread localizado en la cola de espera.

Compilación:


  1. csc /target:exe AccesoArchivoConMonitor.cs

Ejecución assembly:


  1. .\AccesoArchivoConMonitor.exe

> Prueba de ejecución (local):
Ejecución assembly AccesoArchivoConMonitor.exe
Figura 1. Ejecución assembly AccesoArchivoConMonitor.exe.

Notemos que por cada ejecución el orden de ejecución varía, debido a la naturaleza estocástica de ejecución procesos paralelos.

Contenido del archivo nombres-threads.txt [enlace alternativo]:

 3.1.2 Uso de Wait y Pulse

Utilizaremos los métodos Wait y Pulse para simular el sonido tic-tac de un reloj como texto.

Archivo C# TicTacPulse.cs [enlace alternativo]:

La clase TicTac (líneas 6-49) está compuesta con los siguientes elementos de programa:
  • El método Tic (líneas 8-27):Este método se encarga de sincronizar y alternar la reproducción del sonido simulado Tic con el sonido Tac del reloj. En la línea 10 iniciamos la región crítica con el uso de la palabra clave lock (la cual discutiremos en recetas o artículos futuros). Con la sentencia if (línea 12) validamos si ya se debe detener la reproducción del sonido Tic.

    Cuando lo anterior no ocurre, entonces se pausa el thread por un segundo (línea 18), y enseguida se muestra el texto Tic. Con la invocación del método Pulse en la línea 22 damos paso o alternamos a la reproducción del sonido simulado Tac.

  • El método Tac (líneas 29-48): Este método opera de forma análoga al método Tic.
En la clase TicTacPulse (líneas 51-100) creamos una instancia estática del simulador de sonidos tic-tac:

public static TicTac ticTac;


Luego, en las líneas 55-79 declaramos el método IniciarReloj. Este método se encarga realizar la invocación a los métodos Tic y Tac dependiendo del curso que tome la evoluación de la sentencia if (línea 57). Se realizará hasta 5 invocaciones de cada uno de estos métodos:

Línea 62ticTac.Tic(true);  

Línea 73ticTac.Tac(true); 


Cuando los ciclos for para estas sentencias finalicen sus iteraciones, se invocarán los métodos con el argumento false:

ticTac.Tic(flase);

ticTac.Tac(false);


Este argumento hace que finalice el bloqueo en cada uno de los métodos y la ejecución de los threads.

Compilación:


  1. csc /target:exe TicTacPulse.cs

Ejecución assembly:


  1. .\TicTacPulse.exe

> Prueba de ejecución (ideone.com).

> Prueba de ejecución (local):
Ejecución assembly TicTacPulse.exe
Figura 2. Ejecución assembly TicTacPulse.exe.

3.1.3 Sincronización de pool de threads

Construyamos un servidor Web para atender solicitudes de visita al blog xCSw.

Archivo C# ServidorBlog.cs [enlace alternativo]:

La clase BlogxCSw (líneas 6-65) representa el blog sobre el servidor. Este blog (sitio) tiene asignados 3 threads (línea 10) que pueden atender 20 usuarios (línea 14). En la línea 18 declaramos el bloqueador de región crítica: locker. El método IngresarAlBlog es el método que posee la lógica de sincronización de acceso a usuarios al blog. Sobre la línea 25 usamos la construcción lock para iniciar el bloque con locker. Con la Invocación de Wait ponemos al objeto en la cola de espera. Al finalizar el desbloqueo de esta región, se muestra el nombre del número de usuario que accedió al blog: línea 30).


En el código cliente (líneas 38-64) ocurren las siguientes acciones:
  • Línea 41: Creación del pool de threads personalizado.
  • Líneas 44-50: Creación de la cantidad de threads representados por la variable constante threads. A cada thread se le asigna un nombre (línea 47), y se especifica que va a ser un thread de segundo plano (línea 48). Y finalmente se inicia su ejecución (línea 49).
  • Líneas 53-61: El ciclo for se encarga de recibir las solicitudes de los 20 usuarios. Con la invocación del método Pulse se mueve cada thread a la cola de preparados para ejecución.
Compilación:


  1. csc /target:exe ServidorBlog.cs

Ejecución assembly:


  1. .\ServidorBlog.exe

> Prueba de ejecución (ideone.com).

> Prueba de ejecución (local):
Ejecución assembly ServidorBlog.exe
Figura 3. Ejecución assembly ServidorBlog.exe.

3.2 Consideraciones importantes de sincronización

Desde [1] se pueden resaltar las siguientes consideraciones acerca del proceso de sincronización:
«Monitors are managed-code synchronization mechanisms that do not rely on any specific operating system primitives. This ensures that your code is portable should you want to run it on a non-Windows platform. This is in contrast to the synchronization mechanisms (...), which rely on Win32 operating system-base synchronization objects.»
«Because Monitor is used so frequently in multithreaded applications, C# provides language-level support through the lock statement, which the compiler translates to the use of the Monitor class. A block of code encapsulated in a lock statement is equivalent to calling Monitor.Enter when entering the block and Monitor.Exit when exiting the block. In addition, the compiler automatically places the Monitor.Exit call in a finally block to ensure that the lock is realed if an exception is thrown.»

4. Práctica: Código C#

Adaptemos el código de ejemplo encontrado en [1] para afianzar los conceptos aprendidos en la sección anterior.


Compilación:


  1. csc /target:exe SincronizacionConsola.cs

Ejecución assembly:


  1. .\SincronizacionConsola.exe

> Prueba de ejecución (ideone.com).

> Prueba de ejecución (local):
Ejecución assembly SincronizacionConsola.exe
Figura 4. Ejecución assembly SincronizacionConsola.exe.

5. Conclusiones

Hemos preparado esta receta para comprender el proceso de sincronización de threads en ejecución a través de la clase Monitor. Este mecanismo de sincronización protege a recursos o datos compartidos de la corrupción o de la generación de inconsistencia cuando múltiples threads intentan acceder a la misma región de código. A pesar de ser un tema denso y de nivel avanzado, nos adentramos y comprendimos varios de los principios y conceptos prácticos; seguro que con esto en bolsillo, en próximas recetas o artículos, o aplicaciones nos dará más poder de comprensión y práctica. En la próxima receta comprenderemos el mecanismos de sincronización usando Eventos.

Glosario

  • Bloqueador
  • Datos compartidos
  • Locker
  • Recursos compartidos
  • Sección crítica
  • Sincronización

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]: Monitor Class (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.monitor(v=vs.110).aspx
[3]: Monitor.Enter Method (Object) (System.Threading) - http://msdn.microsoft.com/en-us/library/de0542zz(v=vs.110).aspx
[4]: Thread Synchronization (C# and Visual Basic) - http://msdn.microsoft.com/en-us/library/ms173179.aspx
[5]: Receta Multithreading en C# No. 1-5: Obtener el Estado de un Thread | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/07/receta-multithreading-en-csharp-no-1-5-obtener-el-estado-de-un-thread.html
[6]: Monitor.Exit Method (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.monitor.exit(v=vs.110).aspx
[7]: Monitor.Wait Method (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.monitor.wait(v=vs.110).aspx
[8]: Monitor.Pulse Method (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.monitor.pulse(v=vs.110).aspx
[9]: Monitor.PulseAll Method (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.monitor.pulseall(v=vs.110).aspx
[10]: StreamWriter Class (System.IO) - http://msdn.microsoft.com/en-us/library/system.io.streamwriter(v=vs.110).aspx


M

No hay comentarios:

Publicar un comentario

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