El problema Productor/Consumidor

Anuncio
El problema Productor/Consumidor
El problema Productor/Consumidor es uno de los ejemplos clásicos de acceso a recursos compartidos que debe arbitrarse mediante algún mecanismo de concurrencia que implemente la exclusión
mútua.
A continuación se proporcionan versiones que implementan la exclusión mútua mediante la utilización de:
Monitores Java: monitores restringidos a una única variable de condición implı́cita.
Monitores Signal and Continue: modelo general de monitores con más de una variable de condición.
El problema Productor/Consumidor consiste en el acceso concurrente por parte de procesos productores y procesos consumidores sobre un recurso común que resulta ser un buffer de elementos. Los
productores tratan de introducir elementos en el buffer de uno en uno, y los consumidores tratan de
extraer elementos de uno en uno.
Para asegurar la consistencia de la información almacenada en el buffer, el acceso de los productores y consumidores debe hacerse en exclusión mútua. Adicionalmente, el buffer es de capacidad
limitada, de modo que el acceso por parte de un productor para introducir un elemento en el buffer
lleno debe provocar la detención del proceso productor. Lo mismo sucede para un consumidor que
intente extraer un elemento del buffer vacı́o.
Los ficheros proporcionados para la versión nativa de Java estan en el directorio ProdConsJava
y los de la versión general de monitores en el directorio ProdConsMonitor. Los ficheros son los
siguientes:
ProducerConsumerTest.java fichero principal
Producer.java clase Producer
Consumer.java clase Consumer
Buffer.java monitor Buffer
CircularQueue.java cola concreta CircularQueue
Queue.java cola abstracta Queue
En el caso de la versión general de monitores se necesitan además los siguientes ficheros para implementar variables de condición:
Semaphore.java clase Semaphore (tal como lo definió E. W. Dijkstra)
Monitor.java monitor de bajo nivel o variable mútex
Condition.java clase Condition. Implementa variables de condición.
La aplicación como tal queda recogida en el fichero ProducerConsumerTest.java. En él se implementa el método void main(String[] args) que ejecuta el conjunto de la aplicación. La ejecución
consiste en la creación de dos objetos de las clases Producer y Consumer, para ası́ operar con dos
productores y dos consumidores. Una vez creadas las instancias de cada clase, se invoca el método
start() de cada productor y consumidor para iniciar la ejecución en paralelo de cada uno de estos
Threads.
La clase Producer implementa la lógica de un proceso productor. Dentro del método run()
produce un nuevo elemento de tipo Object y a continuación lo introduce en el buffer invocando al
método Put(Object) de la clase Buffer definido para ello. Entre elemento y elemento introducido
se hace dormir al proceso un tiempo aleatorio.
La clase Consumer implementa la lógica de un proceso consumidor. El cometido del proceso es
extraer un elemento del buffer invocando al método Object Get() de la clase Buffer definido para
ello, a continuación el proceso se hace dormir un tiempo aleatorio.
La clase Buffer es un monitor de usuario. Arbitra el acceso concurrente por parte de productores
y consumidores mediante la ejecución en exclusión mútua de los métodos correspondientes. Obsérvese
la diferencia entre el modelo nativo Java y el modelo general. En particular la clase Buffer define
los siguientes métodos:
public /*synchronized (sólo Java nativo)*/ Object Get()
public /*synchronized (sólo Java nativo)*/ void Put(Object value)
El constructor de la clase Buffer mantiene en realidad una instancia de la clase abstracta Queue
que toma como valor concreto una instancia de la clase concreta CircularQueue que se pasa como
parámetro.
La clase CircularQueue implementa una cola circular de capacidad limitada, y se declara como extensión de la clase Queue, esto es ası́ porque la clase Queue es una clase abstracta que
declara los métodos con los que deberı́a contar cualquier clase que pretenda implementar Buffer.
CircularQueue es en la práctica una implementación de Queue aunque serı́an posibles otras implementaciones.
Los métodos definidos en la clase CircularQueue son:
public int Rank() tamaño ocupado de la cola
public int Free() espacio libre en la cola
public boolean IsFull() indicación de cola llena
public void Put(Object value) método de introducción de un elemento en la cola
public Object Get() método de extracción de un elemento de la cola
En esta ocasión los métodos no son declarados sincronizados porque la lógica de resolución de la
exclusión mútua queda reservada para la clase Buffer. La clase CircularQueue se encarga exclusivamente de la lógica de almacenamiento y recuperación de elementos de la estructura de datos
definida para ello.
1. Probar el ejemplo
2. Supóngase que la implementación es tal que los Threads bloqueados en una variable de condición readquieren el monitor por orden de bloqueo (la cola de Threads de la variable de condición
es FIFO). ¿Es posible obtener una traza como la siguiente?:
Consumer
Consumer
Producer
Producer
Producer
Producer
Consumer
Consumer
#0:
#1:
#0:
#0:
#0:
#0:
#1:
#0:
[ get
[ get
[ put
]
[ put
]
56 ]
55 ]
...
...
55 ...
56 ...
Si es asi, ¿cómo corregiria el código proporcionado para que las trazas salieran en orden?
El problema Productor/Consumidor con broadcast
En el problema Productor/Consumidor con broadcast todos los consumidores obtienen todos los
elementos en el orden en que son producidos por los productores. Todos los consumidores obtienen los
elementos en el mismo orden. Dicho de otro modo, cada vez que un productor produce un elemento
lo envia (hace un broadcast) a cada uno de los consumidores.
En esta variante el método Get queda declarado como Object Get(int id) donde id identifica
al consumidor correspondiente.
Solucinar el problema según el criterio:
1. Sin usar monitores Buffer del problema Productor/Consumidor clásico. Programe un monitor
que encapsule NC arrays de elementos, tantos como consumidores. Supóngase 1 productor y
NC consumidores (fácil). Supónganse ahora NP productores (intermedio).
2. Sin usar monitores Buffer del Problema Productor/Consumidor clásico. Reprograme el apartado anterior con un monitor que encapsule un único array de elementos (difı́cil).
3. Usando solo monitores Buffer del problema Productor/Consumidor clásico. Este monitor no se
puede modificar. Supóngase 1 productor y NC consumidores (fácil).
4. Supónganse ahora NP productores. Si la solución del apartado anterior tiene algún problema
¿Cómo la modificaria? (intermedio).
El problema Productor/Consumidor con secuencias atómicas
de operaciones Put/Get
En el problema Productor/Consumidor con secuencias atómicas de operaciones Put/Get los
productores realizan secuencias de operaciones Put no entrelazables con otras operaciones Put de
otros productores y los consumidores realizan secuencias de operaciones Get no entrelazables con
otras operaciones Get de otros consumidores.
Suponiendo 2 productores y 2 consumidores una ejecución correcta es:
a1 a2 a3 --->|
|---> b1 b2 a1
+->-|
b1 b2 --->|
|---> a2 a3
y una incorrecta es:
a1 a2 a3 --->|
|---> a1 b1 b2
+->-|
b1 b2 --->|
|---> a2 a3
Ahora los métodos quedan declarados como:
void Put(Object[] elems): pone de forma no entrelazada en el buffer los elementos del array
elems.
Object[] Get(int n): obtiene n elementos del buffer.
Programar una solución al problema:
1. usando invocaciones a los métodos Put/Get del problema Productor/Consumidor clásico.
2. como métodos nuevos sin usar invocaciones a los métodos Put/Get del problema Productor/Consumidor clásico.
Generación de números primos
Una manera de obtener números primos en paralelo consiste en disponer de múltiples procesos
trabajadores que interaccionan con un gestor central de la forma que se describe a continuación.
El gestor genera candidatos a número primo para los procesos trabajadores que lo soliciten y
almacena adecuadamente los resultados de estos trabajadores.
Cada proceso trabajador realiza de forma cı́clica los siguientes pasos: Obtención de un candidato,
comprobación de si es primo y notificación del resultado.
Cada trabajador solicita un número candidato a ser primo al gestor. Con cada invocación el gestor
devuelve un nuevo candidato (números impares a partir de 3) en orden creciente a los trabajadores.
El gestor devuelve el valor 0 cuando ya se han obtenido los N primos deseados.
Para determinar si un candidato es primo, el trabajador deberá comprobar si es divisible por los
números primos más pequeños (hasta la raı́z cuadrada del candidato es suficiente). Para ello, cada
trabajador guarda una tabla de los números primos ordenados de menor a mayor y añade un nuevo
número a esta tabla cuando lo necesita para comprobar un candidato. El número primo que necesita
añadir lo consigue del gestor, pasándole el ı́ndice de la tabla del primo que le falta. Es suficiente
con pasar el ı́ndice de la tabla de primos porque la relación de primos es la misma en las tablas que
disponen gestor y trabajadores.
Un trabajador puede ser detenido en una petición de número primo (debido a que el gestor aún
no tiene el primo en la posición solicitada). El sistema no se bloqueará porque todos los primos más
pequeños serán generados tarde o temprano por otros trabajadores.
Una vez determinado si el candidato es primo o no, el trabajador lo indica al gestor. El gestor deberá atender a los resultados de los candidatos en orden creciente con el objetivo de introducirlos en la
tabla (si son primos) de forma ordenada. Esto significa que si el resultado no es el esperado deberá detenerse al trabajador. Cuando el gestor obtenga el resultado esperado entonces deberá avanzarse al
siguiente resultado esperado. Además si el resultado del candidato es primo, el gestor lo introduce
en la tabla e incrementa el correspondiente contador.
Se proporciona el fichero Primers.java (principal) y los ficheros incompletos Treballador.java
(proceso trabajador) y Gestor.java (monitor de usuario que gestiona la obtención de los números
primos). Realizar los siguientes apartados:
1. Completar la solución y probarla.
2. Supóngase que no nos fiamos de los trabajadores a la hora de calcular si un candidato es primo
o no. Dar un mismo candidato a 3 trabajadores y decidir si es primo o no por mayoria.
3. Supongase que pueden haber trabajadores que no completen, es decir que pidan un nuevo
candidato pero no lleguen a informar al gestor sobre si es primo o no. Programar una solución
basada en el wait con timeout que temporice transcurrido un intervalo y evite el bloqueo
indefinido por espera del resultado de un candidato.
Descargar