13 minute read

5.4 Monitores

5.4. MONITORES

Los semáforos proporcionan una herramienta potente y flexible para conseguir la exclusión mutua y para la coordinación de procesos. Sin embargo, como la Figura 5.9 sugiere, puede ser difícil producir un programa correcto utilizando semáforos. La dificultad es que las operaciones semWait y semSignal pueden estar dispersas a través de un programa y no resulta fácil ver el efecto global de estas operaciones sobre los semáforos a los que afectan.

Advertisement

El monitor es una construcción del lenguaje de programación que proporciona una funcionalidad equivalente a la de los semáforos pero es más fácil de controlar. El concepto se definió formalmente por primera vez en [HOAR74]. La construcción monitor ha sido implementada en cierto número de lenguajes de programación, incluyendo Pascal Concurrente, Pascal-Plus, Modula-2, Modula-3 y Java. También ha sido implementada como una biblioteca de programa. Esto permite a los programadores poner cerrojos monitor sobre cualquier objeto. En concreto, para algo como una lista encadenada, puede quererse tener un único cerrojo para todas las listas, por cada lista o por cada elemento de cada lista.

Comencemos viendo la versión de Hoare para luego examinar otra más refinada.

MONITOR CON SEÑAL

Un monitor es un módulo software consistente en uno o más procedimientos, una secuencia de inicialización y datos locales. Las principales características de un monitor son las siguientes:

1. Las variables locales de datos son sólo accesibles por los procedimientos del monitor y no por ningún procedimiento externo. 2. Un proceso entra en el monitor invocando uno de sus procedimientos. 3. Sólo un proceso puede estar ejecutando dentro del monitor al tiempo; cualquier otro proceso que haya invocado al monitor se bloquea, en espera de que el monitor quede disponible.

Las dos primeras características guardan semejanza con las de los objetos en el software orientado a objetos. De hecho, en un sistema operativo o lenguaje de programación orientado a objetos puede implementarse inmediatamente un monitor como un objeto con características especiales.

Al cumplir la disciplina de sólo un proceso al mismo tiempo, el monitor es capaz de proporcionar exclusión mutua fácilmente. Las variables de datos en el monitor sólo pueden ser accedidas por un proceso a la vez. Así, una estructura de datos compartida puede ser protegida colocándola dentro de un monitor. Si los datos en el monitor representan cierto recurso, entonces el monitor proporciona la función de exclusión mutua en el acceso al recurso.

Para ser útil para la programación concurrente, el monitor debe incluir herramientas de sincronización. Por ejemplo, suponga un proceso que invoca a un monitor y mientras está en él, deba bloquearse hasta que se satisfaga cierta condición. Se precisa una funcionalidad mediante la cual el proceso no sólo se bloquee, sino que libere el monitor para que algún otro proceso pueda entrar en él. Más tarde, cuando la condición se haya satisfecho y el monitor esté disponible nuevamente, el proceso debe poder ser retomado y permitida su entrada en el monitor en el mismo punto en que se suspendió.

Un monitor soporta la sincronización mediante el uso de variables condición que están contenidas dentro del monitor y son accesibles sólo desde el monitor. Las variables condición son un tipo de datos especial en los monitores que se manipula mediante dos funciones:

• cwait(c): Suspende la ejecución del proceso llamante en la condición c. El monitor queda disponible para ser usado por otro proceso.

• csignal(c): Retoma la ejecución de algún proceso bloqueado por un cwait en la misma condición. Si hay varios procesos, elige uno de ellos; si no hay ninguno, no hace nada.

Nótese que las operaciones wait y signal de los monitores son diferentes de las de los semáforos. Si un proceso en un monitor señala y no hay ningún proceso esperando en la variable condición, la señal se pierde.

La Figura 5.15 ilustra la estructura de un monitor. Aunque un proceso puede entrar en el monitor invocando cualquiera de sus procedimientos, puede entenderse que el monitor tiene un único punto de entrada que es el protegido, de ahí que sólo un proceso pueda estar en el monitor a la vez. Otros procesos que intenten entrar en el monitor se unirán a una cola de procesos bloqueados esperando por la disponibilidad del monitor. Una vez que un proceso está en el monitor, puede temporalmente bloquearse a sí mismo en la condición x realizando un cwait(x); en tal caso, el proceso será añadido a una cola de procesos esperando a reentrar en el monitor cuando cambie la condición y retomar la ejecución en el punto del programa que sigue a la llamada cwait(x).

Área de espera del monitor

Condición c1

cwait(c1) Entrada

MONITOR Cola de entrada de procesos

Datos locales

Variables condición

Procedimiento 1

Condición cn

cwait(cn) Procedimiento k

Cola urgente

csignal Código de inicialización

Salida

Si un proceso que está ejecutando en el monitor detecta un cambio en la variable condición x, realiza un csignal(x), que avisa del cambio a la correspondiente cola de la condición.

Como un ejemplo del uso de un monitor, retornemos al problema productor/consumidor con buffer acotado. La Figura 5.16 muestra una solución utilizando monitores. El módulo monitor, bufferacotado, controla el buffer utilizado para almacenar y extraer caracteres. El monitor incluye dos variables condición (declaradas con la construcción cond): nolleno es cierta cuando hay espacio para añadir al menos un carácter al buffer y novacio es cierta cuando hay al menos un carácter en el buffer.

Un productor puede añadir caracteres al buffer sólo por medio del procedimiento anyadir dentro del monitor; el productor no tiene acceso directo a buffer. El procedimiento comprueba primero la condición nolleno para determinar si hay espacio disponible en el buffer. Si no, el proceso que ejecuta el monitor se bloquea en esa condición. Algún otro proceso (productor o consumidor) puede entrar ahora en el monitor. Más tarde, cuando el buffer ya no esté lleno, el proceso bloqueado podrá ser extraído de la cola, reactivado y retomará su labor. Tras colocar el carácter en el buffer, el proceso señala la condición novacio. Una descripción similar puede realizarse de la función del consumidor.

Este ejemplo marca la división de responsabilidad en los monitores en comparación con los semáforos. En el caso de los monitores, la construcción del monitor impone en sí misma la exclusión mutua: no es posible que ambos, productor y consumidor, accedan simultáneamente al buffer. Sin embargo, el programador debe disponer las primitivas cwait y csignal apropiadas en el código del monitor para impedir que se depositen datos cuando el buffer está lleno o que se extraigan cuando está vacío. En el caso de los semáforos, tanto la exclusión mutua como la sincronización son responsabilidad del programador.

Nótese que en la Figura 5.16 un proceso sale del monitor inmediatamente después de ejecutar la función csignal. Si csignal no sucede al final del procedimiento, entonces, en la propuesta de Hoare, el proceso que emite la señal se bloquea para que el monitor pase a estar disponible, y se sitúa en una cola hasta que el monitor sea liberado. En este punto, una posibilidad sería colocar el proceso bloqueado en la cola de entrada, de manera que tendría que competir por el acceso con otros procesos que no han entrado todavía en el monitor. No obstante, dado que un proceso bloqueado en una función csignal ha realizado ya parcialmente su tarea en el monitor, tiene sentido dar preferencia a este proceso sobre los procesos que entraron recientemente, disponiéndolo en una cola urgente separada (Figura 5.15). Uno de los lenguajes que utiliza monitores, Pascal Concurrente, exige que csignal aparezca solamente como última operación ejecutada por un procedimiento monitor.

Si no hay procesos esperando en la condición x, entonces la ejecución de csignal(x) no tiene efecto.

Como con los semáforos, es posible cometer errores en la función de sincronización de los monitores. Por ejemplo, si se omite alguna de las funciones csignal en el monitor bufferacotado, entonces los procesos que entren en la cola de la correspondiente condición estarán permanentemente colgados. La ventaja que los monitores tienen sobre los semáforos es que todas las funciones de sincronización están confinadas en el monitor. Por tanto, es más fácil comprobar que la sincronización se ha realizado correctamente y detectar los errores. Es más, una vez un monitor se ha programado correctamente, el acceso al recurso protegido será correcto para todo acceso desde cualquier proceso. En cambio, con los semáforos, el acceso al recurso será correcto sólo si todos los procesos que acceden al recurso han sido programados correctamente.

MODELO ALTERNATIVO DE MONITORES CON NOTIFICACIÓN Y DIFUSIÓN

La definición de monitor de Hoare [HOAR74] requiere que si hay al menos un proceso en una cola de una condición, un proceso de dicha cola ejecuta inmediatamente cuando otro proceso realice un

/* programa productor consumidor */ monitor bufferacotado; char buffer[N]; int dentro, fuera; int cuenta; cond nolleno, novacio; void anyadir (char x) {

if (cuenta == N) cwait(nolleno); buffer[dentro] = x; dentro = (dentro + 1) % N; cuenta++; csignal(novacio);

} void extraer (char x) {

if (cuenta == 0) cwait(novacio); x = buffer[fuera]; fuera = (fuera + 1) % N; cuenta—; csignal(nolleno);

dentro = 0; fuera = 0; cuenta = 0; /* espacio para N datos */ /* punteros al buffer */ /* número de datos en el buffer */ /* variables condición para sincronizar */

/* buffer lleno, evitar desbordar */

/* un dato más en el buffer */ /* retoma algún consumidor en espera */

/* buffer vacío, evitar consumo */

/* un dato menos en el buffer */ /* retoma algún productor en espera */

/* cuerpo del monitor */ /* buffer inicialmente vacío */

void productor() {

char x; while (true) {

producir(x); anyadir(x);

} void consumidor() {

char x; while (true) {

extraer(x); consumir(x);

} void main() {

paralelos (productor, consumidor);

csignal sobre dicha condición. Así, el proceso que realiza el csignal debe bien salir inmediatamente del monitor o bien bloquearse dentro del monitor.

Hay dos desventajas en esta solución:

1. Si el proceso que realiza el csignal no ha terminado con el monitor, entonces se necesitarán dos cambios de proceso adicionales: uno para bloquear este proceso y otro para retomarlo cuando el monitor quede disponible. 2. La planificación de procesos asociada con una señal debe ser perfectamente fiable. Cuando se realiza un csignal, un proceso de la cola de la correspondiente condición debe ser activado inmediatamente y el planificador debe asegurar que ningún otro proceso entra en el monitor antes de la activación. De otro modo, la condición bajo la cual el proceso fue activado podría cambiar. Por ejemplo, en la Figura 5.16, cuando se realiza un csignal(novacio), debe ser activado un proceso de la cola novacio antes de que un nuevo consumidor entre en el monitor. Otro ejemplo: un proceso productor puede añadir un carácter a un buffer vacío y entonces fallar justo antes de la señalización; los procesos de la cola novacio estarían permanentemente colgados.

Lampson y Redell desarrollaron una definición diferente de monitor para el lenguaje Mesa [LAMP80]. Su solución resuelve los problemas que se acaban de enunciar y aporta varias extensiones útiles. La estructura del monitor de Mesa se utiliza también en el lenguaje de programación de sistemas Modula-3 [NELS91]. En Mesa, la primitiva csignal se sustituye por la cnotify, con la siguiente interpretación: cuando un proceso ejecutando un monitor ejecuta cnotify(x), provoca que la cola de la condición x sea notificada, pero el proceso que señaló continúa ejecutando. El resultado de la notificación es que el proceso en cabeza de la cola de la condición será retomado en un momento futuro conveniente, cuando el monitor esté disponible. Sin embargo, como no hay garantía de que algún otro proceso entre en el monitor antes que el proceso notificado, el proceso notificado deberá volver a comprobar la condición. Por ejemplo, los procedimientos en el monitor bufferacotado tendrían ahora el código de la Figura 5.17.

void anyadir (char x) {

while (cuenta == N) cwait(nolleno); buffer[dentro] = x; dentro = (dentro + 1) %N; cuenta++; cnofify(novacio);

} void extraer (char x) {

while (cuenta == 0) cwait(novacio); x = buffer[fuera]; fuera = (fuera + 1) %N; cuenta—; cnotify(nolleno); /* buffer lleno, evitar desbordar */

/* un dato más en el buffer */ /* notifica a algún consumidor en espera */

/* buffer vacío, evitar consumo */

/* un dato menos en el buffer */ /* notifica a algún productor en espera */

Las sentencias if se remplazan por bucles while. Así, este convenio requiere al menos una evaluación extra de la variable condición. Acambio, sin embargo, no hay cambios de proceso extra ni tampoco hay restricciones sobre cuando, tras el cnotify, debe ejecutar el proceso notificado.

Una mejora útil que puede asociarse con la primitiva cnotify es un temporizador asociado con cada primitiva de condición. Un proceso que haya estado esperando el máximo del intervalo de tiempo indicado, será situado en estado Listo con independencia de que la condición haya sido notificada. Cuando sea activado, el proceso comprobará la condición y continuará si la condición se satisfizo. La temporización evita la inanición indefinida de un proceso en el caso de que algún otro proceso falle antes de señalar la condición.

Con la norma de que un proceso se notifica en vez de que se reactiva por la fuerza, es posible añadir la primitiva cbroadcast al repertorio. La difusión (broadcast) provoca que todos los procesos esperando en una condición pasen a estado Listo. Esto es conveniente en situaciones donde un proceso no sabe cuántos otros procesos deben ser reactivados. Por ejemplo, en el programa productor/consumidor, suponga que ambas funciones anyadir y extraer puedan aplicarse a bloques de caracteres de longitud variable. En este caso, si un productor añade un bloque de caracteres al buffer, no necesita saber cuántos caracteres está dispuesto a consumir cada consumidor en espera. Simplemente emite un cbroadcast y todos los procesos en espera serán avisados para que lo intenten de nuevo.

En suma, puede usarse un cbroadcast cuando un proceso tenga dificultad en conocer de manera precisa cuántos otros procesos debe reactivar. Un buen ejemplo es un gestor de memoria. El gestor tiene j bytes libres; un proceso libera k bytes adicionales, pero no se sabe si algún proceso en espera puede seguir con un total de k + j bytes. Por tanto utiliza la difusión y todos los procesos verifican por sí mismos si hay suficiente memoria libre.

Una ventaja de los monitores de Lampson/Redell sobre los monitores de Hoare es que la solución de Lampson/Redell es menos propensa a error. En la solución de Lampson/Redell, dado que, al usarse la construcción while, cada procedimiento comprueba la variable condición después de ser señalado, un proceso puede señalar o difundir incorrectamente sin causar un error en el programa señalado. El programa señalado comprobará la variable relevante y si la condición no se cumple, volverá a esperar.

Otra ventaja del monitor Lampson/Redell es que se presta a un enfoque más modular de la construcción de programas. Por ejemplo, considere la implementación de la reserva de un buffer de E/S. Hay dos niveles de condiciones que deben ser satisfechas por los procesos secuenciales cooperantes:

1. Estructuras de datos concordantes. Así, el monitor cumple la exclusión mutua y completa una operación de entrada o salida antes de permitir otra operación sobre el buffer. 2. Nivel 1, más suficiente memoria para que este proceso pueda completar su solicitud de reserva.

En el monitor de Hoare, cada señal transporta la condición de nivel 1 pero también lleva un mensaje implícito, «He liberado suficientes bytes para que tu llamada de solicitud de reserva pueda ahora funcionar». Así, la señal lleva implícita la condición de nivel 2. Si el programador cambia más tarde la definición de la condición de nivel 2, será necesario reprogramar todos los procesos que señalizan. Si el programador cambia las suposiciones realizadas por cualquier proceso en espera concreto (esto es, esperar por un invariante ligeramente diferente al nivel 2), podría ser necesario reprogramar todos los procesos que señalizan. Esto no es modular y es propenso a causar errores de sincronización (por ejemplo, despertar por error) cuando se modifica el código. El programador ha de acordarse de modificar todos los procedimientos del monitor cada vez que se realiza un pequeño cambio en la condición de nivel 2. Con un monitor Lampson/Redell, una difusión asegura la condición de nivel 1 e insinúa que puede que se cumpla la de nivel 2; cada proceso debe comprobar la condición de nivel 2 por

This article is from: