Comedor problema de los filósofos

format_list_bulleted Contenido keyboard_arrow_down
ImprimirCitar
Problema utilizado para ilustrar las cuestiones y técnicas de sincronización para resolverlas
Ilustración del problema de los filósofos del comedor

En informática, el problema de los filósofos comedores es un problema de ejemplo que se utiliza a menudo en el diseño de algoritmos concurrentes para ilustrar problemas de sincronización y técnicas para resolverlos.

Edsger Dijkstra lo formuló originalmente en 1965 como un ejercicio de examen para estudiantes, presentado en términos de computadoras que compiten por el acceso a periféricos de unidades de cinta. Poco después, Tony Hoare le dio al problema su forma actual.

Enunciado del problema

Cinco filósofos cenan juntos en la misma mesa. Cada filósofo tiene su propio lugar en la mesa. Hay un tenedor entre cada plato. El plato que se sirve es una especie de espagueti que hay que comer con dos tenedores. Cada filósofo sólo puede pensar y comer alternativamente. Además, un filósofo solo puede comer sus espaguetis cuando tiene un tenedor derecho e izquierdo. Por lo tanto, dos tenedores solo estarán disponibles cuando sus dos vecinos más cercanos estén pensando, no comiendo. Después de que un filósofo individual termine de comer, dejará ambos tenedores. El problema es cómo diseñar un régimen (un algoritmo concurrente) tal que ningún filósofo muera de hambre; es decir,, cada uno puede seguir alternando eternamente entre comer y pensar, suponiendo que ningún filósofo pueda saber cuándo los demás pueden querer comer o pensar (una cuestión de información incompleta).

Problemas

El problema se diseñó para ilustrar los desafíos de evitar un punto muerto, un estado del sistema en el que no es posible ningún progreso. Para ver que una solución adecuada a este problema no es obvia, considere una propuesta en la que se instruye a cada filósofo para que se comporte de la siguiente manera:

  • pensar a menos que el tenedor izquierdo esté disponible; cuando sea, recogerlo;
  • pensar a menos que el tenedor adecuado esté disponible; cuando sea, recogerlo;
  • cuando ambas horquillas se llevan a cabo, comer por una cantidad fija de tiempo;
  • baje el tenedor izquierdo;
  • baje el tenedor derecho;
  • repetir desde el principio.

Sin embargo, cada uno de ellos pensará durante un tiempo indeterminado y puede terminar sosteniendo un tenedor izquierdo pensando, mirando el lado derecho del plato, sin poder comer porque no hay un tenedor derecho, hasta que se mueran de hambre.

La escasez de recursos, la exclusión mutua y el bloqueo dinámico son otros tipos de problemas de secuencia y acceso.

Soluciones

La solución de Dijkstra

La solución de Dijkstra usa una exclusión mutua, un semáforo por filósofo y una variable de estado por filósofo. Esta solución es más compleja que la solución de jerarquía de recursos. Esta es una versión C++20 de la solución de Dijkstra con los cambios de Tanenbaum:

#include Identificador#include ■iostream#include - No.#include - No.#include Identificado#include - No.constexpr const size_t N = 5; // número de filósofos (y tenedores)enum clase Estado {} Pensando = 0, // filósofo está pensando HUNGRY = 1, // filósofo está tratando de conseguir tenedores EATING = 2, // filósofo es EATING};size_t inline izquierda()size_t i) {}  // número del vecino izquierdo del filósofo i, para quien ambos tenedor están disponibles retorno ()i - 1 + N) % N; // N se añade para el caso cuando yo - 1 es negativo}size_t inline derecho()size_t i) {}  // número del vecino derecho del filósofo i, para quien ambos tenedores están disponibles retorno ()i + 1) % N;}Estado estado[N]; / / / array para hacer un seguimiento de todo el estado disponiblestd::mutex critical_region_mtx; // exclusión mutua para las regiones críticas // (Apilar und derribando las horquillas)std::mutex output_mtx; // para el cout sincronizado (impresión de THINKING/HUNGRY/EATING status)/ / / matriz de semaforas binarias, una semafora por filósofo.// Semaforo requerido significa filósofo que he adquirido (blocked) dos tenedoresstd::binario_semaphore Ambos_forks_disponible[N]{} std::binario_semaphore{}0} std::binario_semaphore{}0} std::binario_semaphore{}0} std::binario_semaphore{}0} std::binario_semaphore{}0}};size_t my_rand()size_t min, size_t max) {} estática std::mt19937 rnd()std::tiempo()nullptr)); retorno std::uniform_int_distribución()min, max)rnd);}vacío prueba()size_t i) // si el filósofo yo tiene hambre y los dos vecinos no comen luego comen{}  // i: número filósofo, de 0 a N-1 si ()estado[i] == Estado::HUNGRY " estado[izquierda()i) ! Estado::EATING " estado[derecho()i) ! Estado::EATING)  {} estado[i] = Estado::EATING; Ambos_forks_disponible[i].liberación(); // Los tenedores ya no son necesarios para esta sesión de comida }}vacío pensar()size_t i) {} size_t duración = my_rand()400, 800); {} std::Lock_guard.std::mutex lk()output_mtx); // sección crítica para impresión ininterrumpida std::Cout .. i .. "está pensando" .. duración .. "Msn"; } std::Esto...::sueño_para()std::crono::milliseconds()duración));}vacío Take_forks()size_t i){} {} std::Lock_guard.std::mutex lk{}critical_region_mtx}; // entrar en la región crítica estado[i] = Estado::HUNGRY; // hecho record que el filósofo yo es Estado: {} std::Lock_guard.std::mutex lk()output_mtx); // sección crítica para impresión ininterrumpida std::Cout .. "t" .. i .. "es Estado:n"; } prueba()i); // tratar de adquirir (un permiso para) 2 tenedores } // salida región crítica Ambos_forks_disponible[i].adquirir adquiere(); // bloque si no se adquirieron horquillas}vacío comer()size_t i){} size_t duración = my_rand()400, 800); {} std::Lock_guard.std::mutex lk()output_mtx); // sección crítica para impresión ininterrumpida std::Cout .. "tttt" .. i .. "está comiendo" .. duración .. "Msn"; } std::Esto...::sueño_para()std::crono::milliseconds()duración));}vacío put_forks()size_t i) {}   std::Lock_guard.std::mutex lk{}critical_region_mtx}; // entrar en la región crítica estado[i] = Estado::Pensando; // El filósofo ha terminado el Estado: prueba()izquierda()i)); // ver si el vecino izquierdo puede comer ahora prueba()derecho()i)); // ver si el vecino derecho puede comer ahora // salida región crítica al salir de la función}vacío filósofo filósofo()size_t i){}  mientras ()verdadero)  {} // repetir para siempre pensar()i); // filósofo es Estado: Take_forks()i); // adquirir dos tenedor o bloque comer()i); // yum-yum, spaghetti put_forks()i); // poner ambas horquillas de nuevo en la mesa y comprobar si los vecinos pueden comer }}int principal() {} std::Cout .. "dp_14n"; std::jthread t0["] {} filósofo filósofo()0); }); // [ cl] significa cada variable fuera de la lambda subsiguiente  std::jthread t1["] {} filósofo filósofo()1); }); // es capturado por referencia std::jthread t2["] {} filósofo filósofo()2); }); std::jthread t3["] {} filósofo filósofo()3); }); std::jthread t4["] {} filósofo filósofo()4); });}

La función test() y su uso en take_forks() y put_forks() hacen que la solución de Dijkstra esté libre de interbloqueos.

Solución de jerarquía de recursos

Esta solución asigna un orden parcial a los recursos (las bifurcaciones, en este caso) y establece la convención de que todos los recursos se solicitarán en orden y que una sola unidad nunca utilizará dos recursos no relacionados por orden. de trabajo al mismo tiempo. Aquí, los recursos (bifurcaciones) estarán numerados del 1 al 5 y cada unidad de trabajo (filósofo) siempre tomará primero la bifurcación con el número más bajo y luego la bifurcación con el número más alto, de entre las dos bifurcaciones que planea usar. No importa el orden en que cada filósofo deja los tenedores. En este caso, si cuatro de los cinco filósofos cogen simultáneamente su tenedor con el número más bajo, solo quedará sobre la mesa el tenedor con el número más alto, por lo que el quinto filósofo no podrá coger ningún tenedor. Además, solo un filósofo tendrá acceso a ese tenedor con el número más alto, por lo que podrá comer con dos tenedores. Intuitivamente, se puede pensar que esto es tener un jugador "zurdo" filósofo en la mesa, quien, a diferencia de todos los demás filósofos, toma su tenedor primero de la izquierda.

Si bien la solución de jerarquía de recursos evita los puntos muertos, no siempre es práctica, especialmente cuando la lista de recursos necesarios no se conoce por completo de antemano. Por ejemplo, si una unidad de trabajo tiene los recursos 3 y 5 y luego determina que necesita el recurso 2, debe liberar 5, luego 3 antes de adquirir 2 y luego debe volver a adquirir 3 y 5 en ese orden. Los programas informáticos que acceden a una gran cantidad de registros de la base de datos no se ejecutarían de manera eficiente si se les solicitara que liberaran todos los registros con números más altos antes de acceder a un nuevo registro, lo que hace que el método no sea práctico para ese propósito.

La solución de la jerarquía de recursos no es justa. Si el filósofo 1 es lento para tomar un tenedor, y si el filósofo 2 es rápido para pensar y volver a tomar sus tenedores, entonces el filósofo 1 nunca podrá tomar ambos tenedores. Una solución justa debe garantizar que cada filósofo finalmente comerá, sin importar qué tan lento se mueva ese filósofo en relación con los demás.

El siguiente código fuente es una implementación C++11 de la solución de jerarquía de recursos para cinco filósofos. La función sleep_for() simula el tiempo que normalmente se dedica a la lógica empresarial.

Para GCC: compilar con

g++ src.cpp -...estado=c+11 - Pan de latón
#include ■iostream#include Identificador#include - No.#include - No.#include - No.#include Identificadoutilizando namespace std;int myrand()int min, int max) {} estática mt19937 rnd()tiempo()nullptr)); retorno uniform_int_distribución()min,max)rnd);}vacío filósofo filósofo()int ph, mutex" ma, mutex" mb, mutex" mo) {} para (;) {} // evitar el hilo de terminación int duración = myrand()200, 800); {} // Bloque { } límites de alcance de bloqueo Lock_guard.mutex gmo()mo); Cout..ph.."Piensa"..duración.."Msn"; } Esto...::sueño_para()crono::milliseconds()duración)); {} Lock_guard.mutex gmo()mo); Cout.."t"..ph.."tiene hambren"; } Lock_guard.mutex gma()ma); // sleep_for() El retraso antes de buscar el segundo tenedor se puede añadir aquí pero no debe ser necesario. Lock_guard.mutex gmb()mb); duración = myrand()200, 800); {} Lock_guard.mutex gmo()mo); Cout.."tttt"..ph.."come"..duración.."Msn"; } Esto...::sueño_para()crono::milliseconds()duración)); }}int principal() {} Cout.."Filosofos C++11 con jerarquía de recursosn"; mutex m1, m2, m3, m4, m5; // 5 tenedores son 5 mutexes mutex mo; // para la salida adecuada // 5 filósofos son 5 hilos hilo t1["] {}filósofo filósofo()1, m1, m2, mo); hilo t2["] {}filósofo filósofo()2, m2, m3, mo); hilo t3["] {}filósofo filósofo()3, m3, m4, mo);  hilo t4["] {}filósofo filósofo()4, m4, m5, mo);  hilo t5["] {}filósofo filósofo()5, m1, m5, mo); // Forzar una jerarquía de recursos t1.Únase(); // evitar que los hilos de terminación t2.Únase(); t3.Únase(); t4.Únase(); t5.Únase();}

Solución de árbitro

Otro enfoque es garantizar que un filósofo solo pueda tomar ambos tenedores o ninguno al presentar un árbitro, por ejemplo, un mesero. Para recoger los tenedores, un filósofo debe pedir permiso al camarero. El camarero le da permiso a un solo filósofo a la vez hasta que el filósofo haya cogido los dos tenedores. Siempre se permite dejar un tenedor. El camarero se puede implementar como un mutex. Además de introducir una nueva entidad central (el camarero), este enfoque puede dar como resultado un paralelismo reducido: si un filósofo está comiendo y uno de sus vecinos pide los tenedores, todos los demás filósofos deben esperar hasta que se cumpla esta solicitud, incluso si los tenedores para ellos todavía están disponibles.

Diseño de algoritmos concurrentes

Limitar el número de comensales en la mesa

Una solución presentada por William Stallings es permitir que un máximo de n-1 filósofos se sienten en cualquier momento. El último filósofo tendría que esperar (por ejemplo, usando un semáforo) a que alguien terminara de cenar antes de "sentarse" y solicitar acceso a cualquier bifurcación. Esto garantiza que al menos un filósofo siempre pueda adquirir ambos tenedores, lo que permite que el sistema progrese.

Solución Chandy/Misra

En 1984, K. Mani Chandy y J. Misra propusieron una solución diferente al problema de los filósofos comedores para permitir agentes arbitrarios (numerados P1,..., Pn) para competir por un número arbitrario de recursos, a diferencia de la solución de Dijkstra. También está completamente distribuido y no requiere una autoridad central después de la inicialización. Sin embargo, viola el requisito de que "los filósofos no se hablan entre ellos" (debido a los mensajes de solicitud).

  1. Para cada par de filósofos que contiendan por un recurso, crear un tenedor y dárselo al filósofo con el ID inferior (n para agente Pn). Cada tenedor puede ser sucio o limpio. Inicialmente, todos los tenedores están sucios.
  2. Cuando un filósofo quiere utilizar un conjunto de recursos (i.e., comer), dijo filósofo debe obtener los tenedores de sus vecinos contendientes. Para todos esos tenedores el filósofo no tiene, envían un mensaje de solicitud.
  3. Cuando un filósofo con tenedor recibe un mensaje de solicitud, guardan el tenedor si está limpio, pero renuncien cuando está sucio. Si el filósofo envía el tenedor, limpian el tenedor antes de hacerlo.
  4. Después de que un filósofo haya terminado de comer, todos sus tenedores se ensucian. Si otro filósofo había solicitado previamente uno de los tenedores, el filósofo que acaba de comer limpia el tenedor y lo envía.

Esta solución también permite un alto grado de simultaneidad y resolverá un problema arbitrariamente grande.

También resuelve el problema del hambre. Las etiquetas limpio/sucio actúan como una forma de dar preferencia a los más "hambrientos" procesos, y una desventaja para los procesos que acaban de "comer". Se podría comparar su solución con una en la que a los filósofos no se les permite comer dos veces seguidas sin dejar que otros usen los tenedores en el medio. La solución de Chandy y Misra es más flexible que eso, pero tiene un elemento que tiende en esa dirección.

En su análisis, derivan un sistema de niveles de preferencia a partir de la distribución de las bifurcaciones y sus estados limpio/sucio. Muestran que este sistema puede describir un gráfico acíclico dirigido y, de ser así, las operaciones en su protocolo no pueden convertir ese gráfico en uno cíclico. Esto garantiza que no se produzca un interbloqueo. Sin embargo, si el sistema se inicializa en un estado perfectamente simétrico, como todos los filósofos que sostienen sus bifurcaciones del lado izquierdo, entonces el gráfico es cíclico desde el principio y su solución no puede evitar un punto muerto. Inicializar el sistema para que los filósofos con ID más bajos tengan bifurcaciones sucias asegura que el gráfico sea inicialmente acíclico.

Contenido relacionado

Tu nombre

uname es un programa de computadora en Unix y sistemas operativos similares a Unix que imprime el nombre, la versión y otros detalles sobre la máquina...

Proceso racional unificado

El proceso unificado racional es un marco de proceso de desarrollo de software iterativo creado por Rational Software Corporation, una división de IBM desde...

SableCC

SableCC es un generador de compiladores de código abierto en Java. La versión estable tiene la licencia GNU Lesser General Public License (LGPL). La...
Más resultados...
Tamaño del texto:
Copiar