En la programación paralela, los monitores son estructuras de datos abstractas destinadas a ser usadas sin peligro por más de un hilo de ejecución. La característica que principalmente los define es que sus métodos son ejecutados con exclusión mutua. Lo que significa, que en cada momento en el tiempo, un hilo como máximo puede estar ejecutando cualquiera de sus métodos. Esta exclusión mutua simplifica el razonamiento de implementar monitores en lugar de código a ser ejecutado en paralelo.
En el estudio y uso de los semáforos se puede ver que las llamadas a las funciones necesarias para utilizarlos quedan repartidas en el código del programa, haciendo difícil corregir errores y asegurar el buen funcionamiento de los algoritmos. Para evitar estos inconvenientes se desarrollaron los monitores. El concepto de monitor fue definido por primera vez por Charles Antony Richard Hoare en un artículo del año 1974.[1] La estructura de los monitores se ha implementado en varios lenguajes de programación, incluido Pascal concurrente, Modula-2, Modula-3 y Java, y como biblioteca de programas.
Un monitor tiene cuatro componentes: inicialización, datos privados, métodos del monitor y cola de entrada.
Los monitores están pensados para ser usados en entornos multiproceso o multihilo, y por lo tanto muchos procesos o hilos pueden llamar a la vez a un procedimiento del monitor. Los monitores garantizan que en cualquier momento, a lo sumo un thread puede estar ejecutando dentro de un monitor. Ejecutar dentro de un monitor significa que solo un thread estará en estado de ejecución mientras dura la llamada a un procedimiento del monitor. El problema de que dos threads ejecuten un mismo procedimiento dentro del monitor es que se pueden dar condiciones de carrera, perjudicando el resultado de los cálculos. Para evitar esto y garantizar la integridad de los datos privados, el monitor hace cumplir la exclusión mutua implícitamente, de modo que solo un procedimiento esté siendo ejecutado a la vez. De esta forma, si un thread llama a un procedimiento mientras otro thread está dentro del monitor, se bloqueará y esperará en la cola de entrada hasta que el monitor quede nuevamente libre. Aunque se la llama cola de entrada, no debería suponerse ninguna política de encolado.
Para que resulten útiles en un entorno de concurrencia, los monitores deben incluir algún tipo de forma de sincronización. Por ejemplo, supóngase un thread que está dentro del monitor y necesita que se cumpla una condición para poder continuar la ejecución. En ese caso, se debe contar con un mecanismo de bloqueo del thread, a la vez que se debe liberar el monitor para ser usado por otro hilo. Más tarde, cuando la condición permita al thread bloqueado continuar ejecutando, debe poder ingresar en el monitor en el mismo lugar donde fue suspendido. Para esto los monitores poseen variables de condición que son accesibles solo desde adentro. Existen dos métodos para operar con las variables de condición:
Nótese que, al contrario que los semáforos, la llamada a cond_signal(c) se pierde si no hay tareas esperando en la variable de condición c.
Las variables de condición indican eventos, y no poseen ningún valor. Si un thread tiene que esperar que ocurra un evento, se dice espera por (o en) la variable de condición correspondiente. Si otro thread provoca un evento, simplemente utiliza la función cond_signal con esa condición como parámetro. De este modo, cada variable de condición tiene una cola asociada para los threads que están esperando que ocurra el evento correspondiente. Las colas se ubican en el sector de datos privados visto anteriormente.
La política de inserción de procesos en las colas de las variables condición es la FIFO, ya que asegura que ningún proceso caiga en la espera indefinida, cosa que sí ocurre con la política LIFO (puede que los procesos de la base de la pila nunca sean despertados) o con una política en la que se desbloquea a un proceso aleatorio.
Antes se dijo que una llamada a la función cond_signal con una variable de condición hacía que un proceso que estaba esperando por esa condición reanudara su ejecución. Nótese que el thread que reanuda su ejecución necesitará obtener nuevamente el lock del monitor. Surgen las siguientes preguntas: ¿Qué sucede con el thread que hizo el cond_signal? ¿Pierde el lock para dárselo al thread que esperaba? ¿Qué thread continúa con su ejecución? Cualquier solución debe garantizar la exclusión mutua. Según quién continúa con la ejecución, se diferencian dos tipos de monitores: Hoare y Mesa.
En la definición original de Hoare, el thread que ejecuta cond_signal le cede el monitor al thread que esperaba. El monitor toma entonces el lock y se lo entrega al thread durmiente, que reanuda la ejecución. Más tarde cuando el monitor quede libre nuevamente el thread que cedió el lock volverá a ejecutar.
Ventajas:
Desventajas:
Butler W. Lampson y David D. Redell en 1980 desarrollaron una definición diferente de monitores para el lenguaje Mesa que lidia con las desventajas de los monitores de tipo Hoare y añade algunas características.
En los monitores de Lampson y Redell el thread que ejecuta cond_signal sobre una variable de condición continúa con su ejecución dentro del monitor. Si hay otro thread esperando en esa variable de condición, se lo despierta y deja como listo. Podrá intentar entrar el monitor cuando éste quede libre, aunque puede suceder que otro thread logre entrar antes. Este nuevo thread puede cambiar la condición por la cual el primer thread estaba durmiendo. Cuando reanude la ejecución el durmiente, debería verificar que la condición efectivamente es la que necesita para seguir ejecutando. En el proceso que durmió, por lo tanto, es necesario cambiar la instrucción if por while, para que al despertar compruebe nuevamente la condición, y de no ser cierta vuelva a llamar a cond_wait.
Además de las dos primitivas cond_wait(c) y cond_signal(c), los monitores de Lampson y Redell poseen la función cond_broadcast(c), que notifica a los threads que están esperando en la variable de condición c y los pone en estado listo. Al entrar al monitor, cada thread verificará la condición por la que estaban detenidos, al igual que antes.
Los monitores del tipo Mesa son menos propensos a errores, ya que un thread podría hacer una llamada incorrecta a cond_signal o a cond_broadcast sin afectar al thread en espera, que verificará la condición y seguirá durmiendo si no fuera la esperada.
La corrección parcial de un monitor se puede demostrar verificando que los invariantes de representación del monitor se cumplen en cualquier caso. El Invariante de representación es el conjunto de estados del monitor que lo hacen correcto. Dicha verificación se puede realizar mediante axiomas y reglas de inferencia como, por ejemplo, los propuestos por la Lógica de Hoare.
El código de inicialización debe incluir la asignación de las variables del monitor antes de que los procedimientos del monitor puedan ser usados. La inicialización de éstas variables debe estar acorde al Invariante de representación del monitor.
Donde IM es el invariante del monitor.
En monitores con señales desplazantes, cond_signal y cond_wait hacen referencia a estados visibles del programa. cond_wait implica la cesión de la exclusión mutua del monitor. Por lo tanto, debe verificar el Invariante del monitor antes de que pueda ejecutarse.
Donde:
En cuanto a la operación signal en señales desplazantes. Cuando se llama a cond_signal, se produce la interrupción inmediata del procedimiento señalador y la reanudación de un proceso bloqueado en la cola de condición. Por lo tanto, todos los invariantes que eran ciertos antes de la ejecución de signal, se mantendrán, igualmente, como ciertos en el proceso señalado, tras la ejecución de cond_wait.
Nótese que la precondición de cond_signal coincide con la postcondición de wait, así como la precondición de cond_wait coincide con la postcondición de signal.
La razón por la cual la condición de desbloqueo, c, no forma parte de la postcondición de signal es que, una vez que el proceso señalado ha reanudado su ejecución, puede hacer falsa ésta condición. Luego, no es posible garantizar que el invariante de la condición de desbloqueo sea cierto.[1]
cond_wait, al igual que con señales desplazantes, bloquea el proceso y cede el uso del monitor. Cuando el proceso en espera vuelva a ser ejecutado, debe garantizarse que el invariante del monitor sigue siendo válido.
Los procesos bloqueados, se desbloquean con cond_signal o con cond_broadcast pero, esta vez, el proceso que llama a cond_signal sigue ejecutándose y utilizando el monitor. Es decir, en este tipo de monitores, señalar a otro proceso no puede provocar un cambio ni en las variables locales el procedimiento ni en las variables del monitor. [2]
En el contexto de los monitores, las reglas de prueba son un conjunto de condiciones establecidas para permitir a un proceso acceder al recurso del monitor. Estas reglas de prueba son definidas con los monitores implementados. Las reglas de prueba van ligadas a otra estructura definida dentro de los monitores: las invariantes. Las invariantes son estados que deben cumplirse en todo momento durante la ejecución de la aplicación para el correcto funcionamiento de los monitores. Así, las invariantes establecen las condiciones que deben cumplirse todo el tiempo y las reglas de prueba establecen las condiciones que deben cumplirse cuando un proceso solicita un recurso del monitor.
Un simple ejemplo de las reglas de prueba es el problema del productor y el consumidor, donde un productor desea continuar produciendo un objeto, pero no puede sobrepasar el número máximo de objetos mientras un consumidor desea consumir el mismo objeto, pero no puede consumir si la lista está vacía. Justamente estas dos condiciones serían las reglas de prueba del monitor. Por otro lado, las invariantes serían los límites de la estructura que almacena los objetos producidos. Juntamente con las variables de condición del monitor, las reglas de prueba son las que permiten al monitor controlar el acceso a los recursos y evitar las condiciones de carrera.