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.