Tema 6. Monitores

Contenidos

1. Introducción

  • Los semáforos nos permiten resolver problemas de sincronización y exclusión mutua, pero…
  • Tienen difícil aplicación en problemas complejos:
    • Basados en variables globales, impiden diseño modular adecuado.
    • ¿Qué semáforos se están utilizando para sincronización y cuáles para exclusión mutua?
    • Operaciones wait y signal dispersas. La supresión/adición de una nueva puede provocar interbloqueos.
    • La mayoría de lenguajes no disponen de mecanismos en tiempo de compilación que nos protejan del uso indebido de las variables empleadas en un programa concurrente.
    • Mantenimiento costoso, en definitiva: propensos a errores

1.1. Concepto de Monitor. Historia.

  • Basada en los trabajos de investigación de Per Brinch Hansen aparece la idea de Monitor como una entidad que resuelve las pegas anteriores.
  • Los Monitores formaron parte de un proyecto más complejo que fue el desarrollo de un lenguaje de programación pensado para la Programación Concurrente. Este lenguaje se llama Concurrent Pascal.
  • En Concurrent Pascal se incluyen los tipos de datos de clase, monitor y proceso.
  • Las clases y los monitores son parecidos:
    • Ambos empaquetan variables privadas y procedimientos con procedimientos públicos (llamados entradas de procedimiento).
    • Una instancia de clase solo puede ser utilizada por un proceso.
    • Una instancia de monitor puede ser compartida por varios procesos.
  • Los monitores proporcionan el único mecanismo para la comunicación entre procesos
  • Hoy en día podríamos comparar los Monitores con una clase con métodos sincronizados de Java.

1.2. Comparativa entre Semáforos y Monitores

  • Semáforos:
    • Recursos globales
    • Código disperso por los procesos
    • Las operaciones sobre los recursos no están restringidas
  • Monitores:
    • Recursos locales
    • Código concentrado
    • Los procesos invocan los procedimientos públicos

sem.png

Figure 1: Comparativa semáforo/monitor.

mon.png

Figure 2: Comparativa semáforo/monitor.

2. Concepto y Funcionamiento

Monitor :
Mecanismo de abstracción de datos: agrupan o encapsulan las representaciones de recursos abstractos y proporcionan un conjunto de operaciones que son las únicas que se pueden realizar sobre dichos recursos.
Operaciones :
Un proceso sólo puede acceder a las variables de monitor usando los procedimientos exportados por el monitor.
Exclusión Mutua :
Garantizada. Un monitor se construye de forma que la ejecución de los procedimientos del monitor (exportados o no) no se solapa.

monstr.png

Figure 3: La estructura de un monitor.

Sintáxis en pseudocódigo

 1: monitor  <nombre>:
 2: 
 3:    (* Variables permanentes, utilizadas para almacenar el estado
 4:    interno del recurso, así como el estado de algún procedimiento
 5:    interno *)
 6:    variables locales
 7: 
 8:    (* Lista de los procedimientos que pueden invocar los procesos
 9:    activos que accedan al monitor. *)
10:    export procedimientos exportados
11: 
12:    (* Implementación de los Procedimientos públicos y privados del
13:    monitor *)
14:    procedure proc1(parámetros):
15:        variables locales;
16:        # código del procedimiento
17: 
18:    procedure proc2(parámetros):
19:        variables locales;
20:        # código del procedimiento
21: 

Ejemplo

  • Supongamos que varios procesos deben incrementar el valor de una variable compartida (en exclusión mutua) y poder examinar su valor en cualquier momento. Para ello definiremos un monitor que encapsulará dicha variable compartida.

     1: monitor incremento:
     2:    integer counter = 0
     3:    export add, value;
     4:    procedure add:
     5:         counter = counter + 1
     6:    procedure  value:
     7:         return counter
     8:    ...
     9: 
    10: incremento.add()
    11: ...
    12: v = incremento.value()
    
  • Los procesos, cuando desean comunicarse o sincronizarse utilizan los recurso privados del Monitor mediante invocación de los procedimientos públicos (exportados).
  • Casos:
    Monitor libre :
    Si un proceso invoca un procedimiento de un monitor y nadie posee el monitor, éste proceso bloquea y ocupa el monitor, (ejecuta el procedimiento)
    Monitor ocupado :
    Si un proceso invoca algún procedimiento del monitor y éste está ocupado entonces el proceso queda bloqueado en una cola asociada al monitor. Cuando el proceso poseedor del monitor finaliza la ejecución de un procedimiento del monitor, se libera el primer proceso bloqueado en ella.
Exclusión Mutua:
Está garantizada por la propia definición del monitor
Sincronización (I) :

Se consigue mediante el uso de Variables de condición declaradas dentro del propio monitor.

Cada una de estas variables dipone de una cola de bloqueo de procesos (aparte de la del monitor)

monsync1.png

Figure 4: Funcionamiento del monitor.

Sincronización (II) :

Esta cola es una cola FIFO:

monsync2.png

Figure 5: Funcionamiento del monitor.

2.1. Variables Condición: Operaciones

  • Delay(c)Bloqueo de procesos:
    • Bloquea el proceso que realiza la llamada al final de la cola asociada a la variable condición c
    • Libera la exclusión mutua antes de bloquearse
  • Resume(c)Desbloqueo de procesos:
    • Extrae el proceso que se encuentra en la cabeza de la cola y lo prepara para su ejecución
    • Cola vacía: operación nula (null)
  • Empty(c)Comprobación de la cola:
    • Devuelve un valor booleano indicando si la cola de la variable condición está vacía o no

monsync3.png

Figure 6: Funcionamiento del monitor.

2.2. Funcionamiento del monitor

Variables de condición

  • Si un proceso ejecutando un procedimiento de un monitor invoca una operación delay(c), libera el monitor y se bloquea en la cola asociada a esa variable de condición.
  • Cuando un proceso ejecutando un procedimiento de un monitor invoca una operación resume(c), se analiza la cola asociada a esa variable de condición, seleccionando al primer proceso bloqueado en ella. Si no hubiese procesos bloqueados la operación no tendrá efecto.
  • ¿Qué pasa si se desbloquea a un proceso? ¿Hay entonces 2 procesos ejecutando en el monitor?: Distintos lenguajes implementan distintos comportamientos (políticas) para la función resume.

2.3. Especificación de prioridades

Hay tres alternativas habituales:

  • Requerimiento de reanudación inmediata (IRR): el proceso bloqueado en la variable condición se debe reanudar inmediatamente:
    • El proceso que señaliza (S) se bloquea inmediatamente y cede el monitor
    • La mayor prioridad la tiene el proceso bloqueado en la condición señalizada (W)
    • Los bloqueados en la entrada del monitor (E) deben esperar
    • Abreviadamente: E \(<\) S \(<\) W
  • El proceso S acaba y sale del monitor, luego los procesos bloqueados en la condición (W) y finalmente los procesos de la entrada (E):
    • Abreviadamente: E \(<\) W \(<\) S
  • La "opción Java":
    • Abreviadamente: E = W \(<\) S

2.4. Ejemplos clásicos: Barreras

  • Monitores con Python:
 1:     mutex = threading.Lock()
 2:     allArrived = threading.Condition(mutex)
 3:     arrived = 0
 4: 
 5:     def barrier(n):
 6:          with mutex:
 7:               arrived += 1
 8:               if arrived == n:
 9:                    arrived = 0
10:                    allArrived.notify_all()
11:               else:
12:                    allArrived.wait()
  • Código completo: barrier.py

2.5. Ejemplos clásicos: Productor/Consumidor

  • Código completo: producer-consumer.py

     1:     # Productor
     2: 
     3:     def append(self, data):
     4:          with mutex:
     5:               while len(buffer) == buffer.maxlen:
     6:                    untilNotFull.wait()     # condición buffer no lleno
     7:               buffer.append(data)
     8:               whileEmpty.notify()
     9: 
    10:     # Consumidor
    11: 
    12:     def take(self):
    13:          with mutex:
    14:               while not buffer:
    15:                    whileEmpty.wait()    # condición buffer no vacío
    16:               data = buffer.popleft() # extrae el primero de la lista
    17:               untilNotFull.notify()
    18:               return data
    

2.6. Ejemplos clásicos: Lectores/Escritores (prioridad Lectores)

  • Código completo: rw_lock.py

     1:     # Lectores
     2: 
     3:     def reader_lock():
     4:          with mutex:
     5:               while writing:
     6:                    canRead.wait()    # condición hay escritores
     7:               readers += 1
     8:               canRead.notify()       # pueden entrar otros lectores
     9: 
    10:     def reader_unlock():
    11:          with mutex:
    12:               readers -= 1
    13:               if not readers:
    14:                    canWrite.notify() # desbloquea escritor si eres el último
    15: 
    16:     # Escritores
    17: 
    18:     def writer_lock():
    19:          with mutex:
    20:               while writing or readers:
    21:                    canWrite.wait()  # condición hay lectores o escritores
    22:               writing = True
    23: 
    24:     def writer_unlock():
    25:          with mutex:
    26:               writers = False
    27:               canRead.notify()  # desbloquea a lectores
    28:               canWrite.notify()  # y escritores
    

2.7. Ejemplos clásicos: Lectores/Escritores (prioridad Escritores)

  • El problema anterior presenta un problema de inanición → los escritores podrían no entrar nunca a la sección crítica
  • Una posible solución es dar preferencia al escritor que esté bloqueado (la función empty habría que definirla usando, por ejemplo, un contador para los escritores en espera):

    1:     def reader_lock():
    2:      with mutex:
    3:           while writing or not empty(canWrite):
    4:                canRead.wait() # condición hay escritores
    5:           readers += 1
    6:           canRead.notify()    # pueden entrar otros lectores
    

2.8. Ejemplos clásicos: Filósofos

 1:   def pick():
 2:      with mutex:
 3:           while picks[i] != 2:
 4:                canEat[i].wait()
 5:           picks[left] -= 1
 6:           picks[right] -= 1
 7: 
 8:   def release():
 9:        with mutex:
10:             picks[left] += 1
11:             picks[right] += 1
12:             if picks[left] == 2:        # sólo desbloquean vecinos con ...
13:                  canEat[left].notify()
14:             if picks[right] == 2:       # ... los dos palillos libres
15:                  canEat[right].notify()
  • Código completo: philosophers.py
  • Una solución similiar en Java: PhilosopherConditions.java

3. Implementación de monitores en Posix

  • Veamos un uso habitual de estas funciones para simular monitores:

     1: pthread_mutex_t crj = PTHREAD_MUTEX_INITIALIZER;
     2: pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
     3: ...
     4: tipoOperacion nombreOperacion(argumentos){
     5:    ...
     6:    pthread_mutex_lock(&crj);
     7:    while(predicado)
     8:         pthread_cond_wait(&cond,&crj);
     9: 
    10:    // Inicio sección crítica
    11:      ...
    12:    // Señalar condiciones si las hubiera
    13:    // Fin de la sección crítica
    14: 
    15:    pthread_mutex_unlock (&crj);
    16:    ...
    17:    return valorDevuelto;  // de tipoOperacion
    18: }
    

Ejemplo: cola con prioridad (ver código)

3.1. Implementación de monitores con semáforos

Elementos :
son los siguientes
  • Para cada monitor:
    semáforo m_mutex :
    inicializado a 1 para la entrada al monitor.
    semáforo m_next :
    para la cola de cortesía inicializado a 0.
    entero m_next_count :
    número de procesos bloqueados en la cola de cortesía (0 inicialmente)
  • Para cada variable de condición (m_x):
    semáforo m_x_sem :
    para la cola de la variable de condición inicializado a 0.
    entero m_x_count :
    número de procesos bloqueados en la variable de condición (0 inicialmente)
Procedimientos exportados por el monitor:

Cada uno de ellos seguirá este patrón:

 1:       wait(m_mutex)
 2:       #  cuerpo_procedimiento
 3:       if m_next_count > 0:
 4:         # Al salir del monitor se debe desbloquear en primer lugar a los
 5:         # procesos de la cola de cortesía si los hay.
 6:         signal(m_next)
 7:         # Fijate que no hay signal(m_mutex) aquí porque hay cesión de la
 8:         # exclusión mutua del proceso que sale al proceso que entra desde la
 9:         # cola de cortesía. Dicho de otro modo, las llamadas a wait y signal
10:         # han de estar equilibradas.
11:       else:
12:         signal(m_mutex)
13: 
  • Operaciones de sincronización (sobre variables de condición)

     1:     empty (m_x):
     2:       if m_x_count == 0:
     3:         return (True)
     4:       else:
     5:         return (False)
     6: 
     7:     resume(m_x):
     8:       if m_x_count != 0:
     9:         m_next_count = m_next_count + 1
    10: 
    11:         signal(m_x_sem) # Libera al siguiente proceso en la cola de la
    12:         wait(m_next)    # variable de condición y se bloquea en la cola
    13:                         # de cortesía
    14: 
    15:         m_next_count = m_next_count - 1
    16: 
    17:     delay(m_x):
    18:       m_x_count = m_x_count + 1
    19: 
    20:       # Alguien puede ocupar el monitor: puede ser de la cola de cortesía
    21:       # primero o de la entrada normal al monitor
    22:       if m_next_count != 0:
    23:          signal(m_next)
    24:          # Libera al siguiente proceso: tienen
    25:          # preferencia los de la cola de cortesía
    26:       else:
    27:         signal(m_mutex)
    28: 
    29:       wait(m_x_sem)     # Se bloquea en la variable de condición m_x
    30:       m_x_count = m_x_count - 1
    

4. Ejercicio Propuesto

Una cuenta de ahorros:
  • Es compartida entre distintas personas (procesos).
  • Cada persona puede sacar o depositar dinero en la cuenta.
  • El balance actual de la cuenta es la suma de los depósitos menos la suma de las cantidades sacadas.
  • El balance nunca puede ser negativo.
Se pide :
Construir un monitor en pseudocódigo para resolver este problema con las operaciones depositar(cantidad) y devolver(cantidad).
  • El cliente que deposita debe despertar a los clientes que están esperando para sacar
  • El cliente que llega para sacar dinero lo saca si existe saldo, independientemente de que haya algún otro proceso esperando porque no hay suficiente dinero para él.
Variante :
Implementad el mismo problema suponiendo que ningún cliente puede colarse para sacar dinero.

5. Ejercicio Propuesto

  • Implementad el problema de la tribu con monitores
    • Los miembros de la tribu cenan en comunidad de una gran olla que contiene M misioneros.
    • Cuando un miembro de la tribu quiere comer, él mismo se sirve de la olla, a menos que esté vacía.
    • Si la olla está vacía, entonces despierta al cocinero y espera a que éste llene la olla.
    • Sólo se debe despertar al cocinero cuando la olla está vacía.

6. Aclaraciones

  • En ningún caso estas transparencias son la bibliografía de la asignatura, por lo tanto debes estudiar, aclarar y ampliar los conceptos que en ellas encuentres empleando los enlaces web y bibliografía recomendada que puedes consultar en la página web de la ficha de la asignatura y en la web propia de la asignatura.

Created: 2023-12-15 vie 13:35

Validate