2.5.2 Monitores Los semáforos, a pesar de su sencillez de uso, 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. 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. 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). 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 1
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). Métodos de Servicio: metodo1(..) metodo2(..) Hilos esperando entrar al monitor : (invocaron a un método de servicio) metodon(..) Datos Globales Variables de Condición: Cond 1 lista de bloqueados por Cond 1 Cond 2 lista de bloqueados por Cond 2 : : Condm lista de bloqueados por Condm Figura 2.2: Estructura de los Monitores. 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). 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; a su vez, 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. 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. En la Figura 2.3 se ilustra el funcionamiento de tales disciplinas (suponemos que el tiempo hacia abajo). 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 2
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. Figura 2.3: Disciplinas de señalamiento en los Monitores. Para la disciplina signal and exit, se asume lo siguiente: 1. 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. 2. 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; 3
rear = (rear+1) % size; count++; if (count == 1) notify(notempty); Programación Concurrente y Paralela 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. 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 fue 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). 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 4
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 ejemplos, se muestran segmentos de código para los problemas productorconsumidor con buffer limitado (prog. Monitor/bbmo.java) y los filósofos comelones (Monitor/dpmo.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; Nótese que no ocurren condiciones de contención sobre la variable compartida count. private void seeifstarving(int k) { if (state[k] == HUNGRY && state[left(k)]!= STARVING && state[right(k)]!= STARVING) { state[k] = STARVING; System.out.println("philosopher " + k + " is STARVING"); private void test(int k, boolean checkstarving) { 5
if (state[left(k)]!= EATING && state[left(k)]!= STARVING && (state[k] == HUNGRY state[k] == STARVING) && state[right(k)]!= STARVING && state[right(k)]!= EATING) state[k] = EATING; else if (checkstarving) seeifstarving(k); // simplistic naive check for starvation Programación Concurrente y Paralela public synchronized void takeforks(int i) { state[i] = HUNGRY; test(i, false); while (state[i]!= EATING) try {wait(); catch (InterruptedException e) { public synchronized void putforks(int i) { state[i] = THINKING; test(left(i), checkstarving); test(right(i), checkstarving); notifyall(); En este programa se utiliza un esquema simple para prevención de inanición. Dado que Java solamente soporta una variable de condición única en sus monitores no se puede utilizar un arreglo de variables de condición (una para cada filósofo) para bloquear a un filosofo hambriento que no puede comer. En lugar de ello, se utiliza notifyall() para despertar a todos los filósofos que estén esperando a que su vecino deje de comer; lo cual es muy ineficiente pues a lo más dos filósofos necesitan ser despertados. Consecuentemente, la mayoría de los métodos de monitores en Java siguen el siguiente patrón de diseño: public synchronized tipo metodo(...) {... notifyall(); // si alguna condición de espera fué alterada while(!condicion) try { wait(); catch (InterruptedException e) {... notifyall(); // si alguna condición de espera fué alterada 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(); 6
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. 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. 7