Monitores
Los semáforos, son el equivalente a las instrucciones goto y el manejo de apuntadores en los lenguajes de programación imperativos: son muy susceptibles a errores. Su utilización exige disciplina.
Por ejemplo, el siguiente error conduce inmediatamente a un deadlock:
P(S) ; sección crítica P(S);
Generalmente resulta difícil distinguir entre los dos usos de los semáforos (i.e. para exclusión mutua y condición de sincronización) en un programa sin una revisión detallada de todo el código.
Los monitores pretenden ayudar a evitar los riesgos a que se presentan esos tipos de errores de programación, proporcionando construcciones de programación de mayor nivel de abstracción que los semáforos, ya que los monitores están en estrecha relación con la programación orientada a objetos, además de ser la primitiva para sincronización interconstruída que ofrece Java.
Definición de monitor
Un monitor es un módulo opaco que encapsula servicios mediante métodos de acceso, así como sus variables locales y globales.
La única forma para manipular o acceder las variables dentro del monitor es invocando alguno de los métodos de servicio.
Solamente se permite que un hilo esté activo a la vez dentro del monitor ejecutando uno de los métodos de servicio, asegurando exclusión mutua y previniendo implícitamente la presencia de condiciones de contención.
Cada objeto monitor tiene un candado, el compilador del lenguaje de programación genera el código al comienzo de cada método de servicio para adquirir el candado y al final para liberarlo.
Si el monitor está ocupado por algún hilo (i.e. se apropió del candado), los hilos siguientes que invoquen alguno de los métodos de servicio del monitor (i.e. intenten entrar al monitor) serán bloqueados e incorporados en la lista de espera para adquirir el candado.
Sincronización
Al igual que los semáforos, los monitores ofrecen
las dos formas de sincronización:
la exclusión mutua está garantizada por el compilador
implícitamente al invocar métodos de servicio.
Para proporcionar mecanismos para sincronización de
eventos (condición de sincronización) un monitor puede
contener variables de condición, las cuales pueden
manipularse mediante las operaciones signal y wait (que
son análogas a las operaciones P y V en semáforos
binarios, respectivamente).
Funcionamiento Su funcionamiento se describe enseguida:
wait: Un hilo que espera a que ocurra un evento indicado por una variable de condición deja al monitor temporalmente, libera el candado y se une a la lista de hilos bloqueados correspondiente a esa variable de condición.
signal: Cada señal con respecto a una variable de condición despierta a un hilo de la lista de hilos bloqueados correspondiente a esa variable de condición (no necesariamente el que lleva más tiempo en espera), si no hay ningún hilo esperando, la señal no se almacena y no tiene efecto (contrastando a la manera en la cual los semáforos funcionan).
Dado que las operaciones de liberar al candado y unirse a la lista de espera por una variable de condición son operaciones atómicas no hay riesgo de pérdida de las señales (i.e. `wakeup'). Al hilo despertado por la señal se le desplaza de esa lista de bloqueados y se le coloca en la lista de hilos en espera por entrar al monitor. Una vez que el candado sea readquirido, el hilo en cuestión continúa la ejecución del método de servicio que invocó anteriormente (i.e. la primera vez para entrar al monitor).
Estructura de un monitor
Las variables de condición de los monitores no tienen valor, se le puede considerar como el nombre de la lista de hilos bloqueados (nombre de un evento).
Diferencias con los semáforos
Note que las variables de condición y los semáforos
difieren en dos maneras:
una señal realizada en una variable cuya lista de hilos
bloqueados está vacía no tiene efecto mientras que la
invocación a V incrementa el contador del semáforo;
una invocación a la primitiva wait en una variable de
condición siempre bloquea al hilo hasta que reciba una
señal mientras que una invocación a P decrementa el
contador del semáforo si su valor es positivo y no bloquea
al hilo.
Disciplinas de señalización
El manejo de las variables de condición de los monitores se implanta de acuerdo a una de las diferentes disciplinas de señalización:
1. signal and exit: si un hilo que esté ejecutándose dentro del monitor emite una señal respecto una variable de condición entonces debe dejar el monitor inmediatamente ejecutando una instrucción return en el método de servicio que invocó. Se despierta a un hilo de la lista de hilos bloqueados correspondiente a la variable de condición y continúa ejecutándose dentro del monitor.
2. signal and wait: el hilo que recibe la señal (señalado o despertado) se ejecuta dentro del monitor, mientras que el hilo que emitió la señal (señalador) espera a que el señalado salga del monitor y entonces pueda continuar el señalador.
3. signal and continue: el hilo despertado espera a que el señalador deje el monitor y entonces continúa su ejecución dentro del monitor.
Disciplinas de señalamiento en los Monitores
Los monitores en Java utilizan la disciplina signal
and continue, pero revisaremos primero a la más
sencilla que es signal and exit.
Los métodos de servicio se indican con la palabra
reservada synchronized indicando que un sólo hilo
se le permite ejecutar el método a la vez, el método
wait(condVar) permite indicar la espera de una
variable de condición, por su parte notify(condVar)
indica la emisión de una señal.
Signal and exit
Para la disciplina signal and exit, se asume lo
siguiente:
Después de emitir una señal respecto una variable de
condición, el señalador debe salir inmediatamente del
monitor, de tal forma que ninguna variable cambia antes de
que el hilo despertado continúe ejecutándose dentro del
monitor. Por tanto, el hilo despertado encuentra que la
condición de la señal es verdadera.
Al hilo despertado se le otorga prioridad para proceder
inmediatamente al monitor sobre aquellos hilos que
estaban esperando entrar al monitor mediante la
invocación de un método de servicio.
Como ejemplo, mostramos (en pseudo código) un fragmento para la solución al problema del productor-consumidor utilizando un buffer limitado (archivo Monitor/bbse.java):
public synchronized void deposit(double data) {
if (count == size) wait(notFull);
buf[rear] = data;
rear = (rear+1) % size;
count++;
if (count == 1) notify(notEmpty);
}
public synchronized double fetch() {
double result;
if (count == 0) wait(notEmpty);
result = buf[front];
front = (front+1) % size;
count--;
if (count == size-1) notify(notFull);
return result;
}
Si el buffer está lleno, el productor se bloquea con
la invocación al método wait respecto a la variable
de condición notFull, el productor es despertado por
el consumidor mediante la señal notify cuando deja
un espacio libre en el buffer.
Si el buffer está vacío, el consumidor se bloquea
respecto la variable notEmpty y a su vez será
despertado por el productor cuando éste coloque un
elemento en el buffer.
Signal and continue Para la disciplina signal and continue, no se requiere que el hilo que emitió la señal salga
del monitor, ni tampoco que el hilo despertado tenga prioridad para proceder dentro del monitor sobre los demás hilos que están esperando entrar (compiten por el candado).
Por ello, no se puede garantizar que la condición que condujo la emisión de la señal continue siendo válida cuando el hilo que fué despertado entre de nuevo al monitor, ya que antes de dejar al monitor el hilo que emitió la señal podrá haber cambiado datos y/o alterado el estado interno del monitor.
A su vez, los hilos que compiten por entrar al monitor pueden adelantárseles a los hilos que están esperando a que ocurra el evento de la variable de condición, lo cual resulta en una forma de inanición (pues el tiempo de espera asociado a la variable de condición no está limitado).
Debido a ello, se deben tomar precauciones para que el hilo que estaba esperando por la variable de condición verifique que ésta haya ocurrido una vez que entra de nuevo al monitor, i.e. fundamentalmente se debe cambiar
if (condicion) wait(); // bloqueo en signal-and-exit
por un bucle:
while (condicion) wait(); // bloqueo en signal-and-continue
incluso permitiendo que la señal pueda despertar a más de un hilo bloqueado (i.e una forma de broadcast).
Monitor en Java Para construir un monitor en Java, se debe utilizar el modificador
synchronized en cada método de servicio (i.e que requiera exclusión mutua), tales métodos generalmente son públicos pero pueden también ser privados.
Cada objeto Java tiene un candado asociado; implícitamente, un hilo que quiera ejecutar un método synchronized de un objeto primero debe apropiarse del candado, bloqueándose si es que está en uso por otro hilo.
Desafortunadamente, cada monitor en Java sólo tiene una variable de condición anónima; todas las invocaciones a wait() y notify() (para bloquear y emitir una señal, respectivamente) se refieren automáticamente a esa variable anónima.
Por su parte, la primitiva notifyAll() permite despertar a todos los hilos que se encuentran bloqueados esperando por la variable de condición anónima.
Como ejemplo, se muestra segmentos de código para el problema productor- consumidor con buffer limitado (prog. Monitor/bbmo.java)
public synchronized void deposit(double value) {
while (count == numSlots)
try {
wait();
} catch (InterruptedException e) {
System.err.println("interrupted out of wait");
}
buffer[putIn] = value;
putIn = (putIn + 1) % numSlots;
count++; // wake up the consumer
if (count == 1) notify(); // since it might be waiting
}
public synchronized double fetch() {
double value;
while (count == 0)
try {
wait();
} catch (InterruptedException e) {
System.err.println("interrupted out of wait");
}
value = buffer[takeOut];
takeOut = (takeOut + 1) % numSlots;
count--; // wake up the producer
if (count == numSlots-1) notify(); // since it might be waiting
return value;
}
Patrón de diseño de los monitores en Java
public synchronized tipo metodo(...) {
...
notifyAll(); // si alguna condición de espera fue alterada
while(!condicion)
try { wait(); }catch (InterruptedException e) {}
...
notifyAll(); // si alguna condición de espera fue alterada
}
Objetos sincronizados
A pesar de que los monitores en Java solamente tienen una variable de condición anónima, se puede utilizar un objeto para implantar una variable de condición con nombre, que actuará como un objeto para notificación:
objeto compartido:
Object obj = new Object();
en un hilo: en el otro:
synchronized(obj) { synchronized(obj) {
if (!cond) if (cond) obj.notify();
try { obj.wait();} ...
catch(InterruptedException e){} }
...
}
Dentro de un bloque de exclusión mutua en
obj un hilo verifica la condición para ver si
puede continuar, de lo contrario espera.
El otro hilo cambia la condición y notifica al
hilo en espera.
Cuando se usa dentro de un monitor, un
objeto de notificación juega el rol de una
variable de condición con nombre.
Deadlocks en monitores Cabe mencionar que las invocaciones anidadas a métodos de servicio de los
monitores están sujetos a deadlocks: class S { class T {
synchronized void f(T t) { synchronized void g(S s) {
... t.g(...); ... ... s.f(...); ...
}} }}
Aquí, s y t son referencias a monitores creados en las clases S y T respectivamente, y que son compartidos por dos hilos A y B en la siguiente secuencia de eventos:
A: invoca s.f(t);
B: invoca t.g(s);
A: se bloquea al invocar t.g
B: se bloquea al invocar s.f
Para evitar deadlocks, se sugiere ordenar globalmente todos los objetos monitor y solicitar que todos los hilos que compiten por el candado del monitor sigan el mismo orden.