Hace algún tiempo, un enlace al artículo "Filósofos de la comida moderna" se extendió sobre recursos como Reddit y HackerNews. El artículo es interesante, muestra varias soluciones a esta tarea conocida, implementada en C ++ moderno utilizando un enfoque basado en tareas. Si alguien aún no ha leído este artículo, entonces tiene sentido pasar tiempo y leerlo.
Sin embargo, no puedo decir que las soluciones presentadas en el artículo me parecieron simples y comprensibles. Esto probablemente se deba al uso de tareas. Demasiados de ellos son creados y enviados a través de una variedad de despachadores / serializadores. Por lo tanto, no siempre está claro dónde, cuándo y qué tareas se realizan.
Además, el enfoque basado en tareas no es el único posible para resolver tales problemas. ¿Por qué no ver cómo se resuelve la tarea de los "filósofos gastronómicos" a través de los modelos de actores y CSP?
Por lo tanto, traté de buscar e implementar varias soluciones a este problema usando Actors y CSP. El código para estas soluciones se puede encontrar en el repositorio en GitHub . Y debajo del cortador, explicaciones y explicaciones, para que cualquiera que esté interesado, sea bienvenido.
Algunas palabras comunes
No tenía el objetivo de repetir exactamente las decisiones que se muestran en el mismo artículo "Filósofos de la cocina moderna" , especialmente porque fundamentalmente no me gusta una cosa importante: de hecho, el filósofo no hace nada en esas decisiones. Él solo dice "Quiero comer", y luego alguien le da tenedores mágicamente o dice "ahora no funcionará".
Está claro por qué el autor recurrió a tal comportamiento: permite el uso de la misma implementación del "filósofo" junto con diferentes implementaciones de los "protocolos". Sin embargo, me parece personalmente que es más interesante cuando el "filósofo" trata de tomar un tapón primero, luego otro. Y cuando el "filósofo" se ve obligado a manejar intentos fallidos de capturar los tenedores.
Precisamente estas realizaciones de la tarea de los "filósofos gastronómicos" fueron las que intenté hacer. Al mismo tiempo, algunas soluciones utilizaron los mismos enfoques que en el artículo mencionado (por ejemplo, implementado por los protocolos ForkLevelPhilosopherProtocol y WaiterFair).
Construí mis decisiones sobre la base de SObjectizer , que es poco probable que sorprenda a quienes leyeron mis artículos antes. Si alguien aún no ha oído hablar de SObjectizer, en pocas palabras: este es uno de los pocos "marcos de actores" de OpenSource en vivo y en desarrollo para C ++ ( CAF y QP / C ++ también se pueden mencionar entre otros). Espero que los ejemplos anteriores con mis comentarios sean lo suficientemente claros incluso para aquellos que no están familiarizados con SObjectizer. Si no, estaré encantado de responder preguntas en los comentarios.
Soluciones de actores
Comenzaremos la discusión de las soluciones implementadas con aquellas basadas en los actores. Primero, considere la implementación de la solución Edsger Dijkstra, luego pase a otras soluciones y vea cómo difiere el comportamiento de cada una de ellas.
La decisión de Dijkstra
Edsger Dijkstra, no solo formuló la tarea de "comer phylophos" (Tony Hoar expresó su formulación utilizando "tenedores" y "spaghetti"), sino que también propuso una solución muy simple y hermosa. A saber: los filósofos solo deben agarrar los tenedores para aumentar el número de tenedores, y si el filósofo logró tomar el primer tenedor, entonces no lo soltará hasta que reciba el segundo tenedor.
Por ejemplo, si un filósofo necesita usar tenedores con los números 5 y 6, entonces un filósofo primero debe tomar un tenedor del número 5. Solo entonces puede tomar un tenedor del número 6. Por lo tanto, si los tenedores con números más bajos están a la izquierda de los filósofos, entonces el filósofo debe primero tome el tenedor izquierdo y solo entonces puede tomar el tenedor derecho.
El último filósofo de la lista, que tiene que lidiar con los tenedores en los números (N-1) y 0, hace lo contrario: primero toma el tenedor derecho con el número 0, y luego el tenedor izquierdo con el número (N-1).
Para implementar este enfoque, se requerirán dos tipos de actores: uno para los tenedores y otro para los filósofos. Si el filósofo quiere comer, envía un mensaje al actor tenedor correspondiente para capturar el tenedor, y el actor tenedor responde con un mensaje de respuesta.
El código para implementar este enfoque se puede ver aquí .
Mensajes
Antes de hablar sobre actores, debe mirar los mensajes que los actores intercambiarán:
struct take_t { const so_5::mbox_t m_who; std::size_t m_philosopher_index; }; struct taken_t : public so_5::signal_t {}; struct put_t : public so_5::signal_t {};
Cuando el actor-filósofo quiere desconectarse, envía el mensaje take_t
al take_t
fork, y el actor fork responde con el mensaje taken_t
. Cuando el actor-filósofo termina de comer y quiere volver a poner los tenedores sobre la mesa, envía mensajes put_t a los tenedores put_t
.
En el mensaje take_t
, el campo take_t
denota el buzón (también conocido como mbox) del actor filósofo. Se debe enviar un mensaje de respuesta taken_t
a este taken_t
. El segundo campo de take_t
no se usa en este ejemplo, lo necesitaremos cuando lleguemos a las implementaciones de waiter_with_queue y waiter_with_timestamps.
Tenedor actor
Ahora podemos ver qué es un actor tenedor. Aquí está su código:
class fork_t final : public so_5::agent_t { public : fork_t( context_t ctx ) : so_5::agent_t{ std::move(ctx) } {} void so_define_agent() override {
Cada actor en SObjectizer debe derivarse de la clase base agent_t
. Lo que vemos aquí para el tipo fork_t
.
El método so_define_agent()
se reemplaza en la clase so_define_agent()
. Este es un método especial, el SObjectizer lo llama automáticamente al registrar un nuevo agente. En el método so_define_agent()
, el so_define_agent()
está "configurado" para trabajar en SObjectizer: el estado de inicio cambia, los mensajes necesarios se suscriben.
Cada actor en SObjectizer es una máquina de estados con estados (incluso si un actor usa solo un estado predeterminado). El actor fork_t
tiene dos estados: libre y tomado . Cuando un actor está en estado libre , el filósofo puede "capturar" el tapón. Y después de capturar el "tenedor", el actor fork_t
debería pasar al estado tomado . Dentro de la clase fork_t
estados están representados por instancias de st_free
y st_taken
tipo especial state_t
.
Los estados le permiten procesar los mensajes entrantes de diferentes maneras. Por ejemplo, en el estado libre , el agente responde solo a take_t
y esta reacción es muy simple: el estado del actor cambia y la respuesta taken_t
:
st_free .event( [this]( mhood_t<take_t> cmd ) { this >>= st_taken; so_5::send< taken_t >( cmd->m_who ); } );
Mientras que todos los demás mensajes, incluido put_t
en estado libre , simplemente se ignoran.
En el estado tomado , el actor procesa dos mensajes, e incluso el mensaje take_t
procesa de manera diferente:
st_taken .event( [this]( mhood_t<take_t> cmd ) { m_queue.push( cmd->m_who ); } ) .event( [this]( mhood_t<put_t> ) { if( m_queue.empty() ) this >>= st_free; else { const auto who = m_queue.front(); m_queue.pop(); so_5::send< taken_t >( who ); } } );
El controlador para put_t
más interesante put_t
: si la cola de los filósofos en espera está vacía, entonces podemos volver a liberar , pero si no está vacía, entonces el primero de ellos debe enviarse taken_t
.
Actor filósofo
El código del actor-filósofo es mucho más voluminoso, por lo que no lo daré aquí por completo. Discutiremos solo los fragmentos más significativos.
Un actor-filósofo tiene un poco más de estados:
state_t st_thinking{ this, "thinking.normal" }; state_t st_wait_left{ this, "wait_left" }; state_t st_wait_right{ this, "wait_right" }; state_t st_eating{ this, "eating" }; state_t st_done{ this, "done" };
El actor comienza su trabajo en un estado de pensamiento , luego cambia a wait_left , luego a wait_right , luego a comer . De comer, un actor puede volver a pensar o puede hacerlo si el filósofo ha comido todo lo que debería haber comido.
El diagrama de estado para un actor-filósofo se puede representar de la siguiente manera:

La lógica del comportamiento del actor se describe en la implementación de su método so_define_agent()
:
void so_define_agent() override {
Quizás el único punto que debe enfatizarse particularmente es el enfoque para imitar los procesos de "pensar" y "comer". No hay this_thread::sleep_for
en el código del actor ni ninguna otra forma de bloquear el hilo de trabajo actual. En cambio, se utilizan mensajes pendientes. Por ejemplo, cuando un actor entra en el estado de comer , se envía un mensaje de stop_eating_t
pendiente. Este mensaje se entrega al temporizador SObjectizer y el temporizador entrega el mensaje al actor cuando llega el momento.
El uso de mensajes retrasados le permite ejecutar a todos los actores en el contexto de un solo hilo de trabajo. En términos generales, un hilo lee los mensajes de alguna cola y tira del siguiente manejador de mensajes del actor receptor correspondiente. Más adelante se discutirá más sobre los contextos de trabajo para los actores.
Resultados
Los resultados de esta implementación pueden verse de la siguiente manera (un pequeño fragmento):
Socrates: tttttttttttLRRRRRRRRRRRRRREEEEEEEttttttttLRRRRRRRRRRRRRREEEEEEEEEEEEE Plato: ttttttttttEEEEEEEEEEEEEEEEttttttttttRRRRRREEEEEEEEEEEEEEttttttttttLLL Aristotle: ttttEEEEEtttttttttttLLLLLLRRRREEEEEEEEEEEEttttttttttttLLEEEEEEEEEEEEE Descartes: tttttLLLLRRRRRRRREEEEEEEEEEEEEtttLLLLLLLLLRRRRREEEEEEttttttttttLLLLLL Spinoza: ttttEEEEEEEEEEEEEttttttttttLLLRRRREEEEEEEEEEEEEttttttttttRRRREEEEEEtt Kant: ttttttttttLLLLLLLRREEEEEEEEEEEEEEEttttttttttLLLEEEEEEEEEEEEEEtttttttt Schopenhauer: ttttttEEEEEEEEEEEEEttttttLLLLLLLLLEEEEEEEEEttttttttLLLLLLLLLLRRRRRRRR Nietzsche: tttttttttLLLLLLLLLLEEEEEEEEEEEEEttttttttLLLEEEEEEEEEttttttttRRRRRRRRE Wittgenstein: ttttEEEEEEEEEEtttttLLLLLLLLLLLLLEEEEEEEEEttttttttttttRRRREEEEEEEEEEEt Heidegger: tttttttttttLLLEEEEEEEEEEEEEEtttttttLLLLLLREEEEEEEEEEEEEEEtttLLLLLLLLR Sartre: tttEEEEEEEEEttttLLLLLLLLLLLLRRRRREEEEEEEEEtttttttLLLLLLLLRRRRRRRRRRRR
Lea esto de la siguiente manera:
t
denota que el filósofo está "pensando";L
significa que el filósofo espera capturar la bifurcación izquierda (está en el estado wait_left );R
significa que el filósofo espera capturar la bifurcación correcta (está en el estado wait_right );E
significa que el filósofo "come".
Podemos ver que Sócrates puede tomar el tenedor a la izquierda solo después de que Sartre lo regala. Después de lo cual Sócrates esperará hasta que Platón libere el tenedor correcto. Solo después de esto, Sócrates podrá comer.
Una decisión simple sin un árbitro (camarero)
Si analizamos el resultado de la decisión de Dijkstra, veremos que los filósofos pasan mucho tiempo esperando la captura de tenedores. Lo que no es bueno, porque Este tiempo también se puede dedicar a la reflexión. No es por nada que hay una opinión de que si piensas con el estómago vacío, puedes obtener resultados mucho más interesantes e inesperados;)
Veamos la solución más simple en la que el filósofo devuelve el primer tenedor capturado si no puede capturar el segundo (en el artículo "Filósofos de la gastronomía moderna" mencionado anteriormente, esta solución es implementada por ForkLevelPhilosopherProtocol).
El código fuente de esta implementación se puede ver aquí , y el código del actor filósofo correspondiente aquí .
Mensajes
Esta solución utiliza casi el mismo conjunto de mensajes:
struct take_t { const so_5::mbox_t m_who; std::size_t m_philosopher_index; }; struct busy_t : public so_5::signal_t {}; struct taken_t : public so_5::signal_t {}; struct put_t : public so_5::signal_t {};
La única diferencia es la presencia de la señal busy_t
. El tenedor-actor envía esta señal en respuesta al filósofo-actor si el tenedor ya está capturado por otro filósofo.
Tenedor actor
El actor tenedor en esta solución es aún más simple que en la solución de Dijkstra:
class fork_t final : public so_5::agent_t { public : fork_t( context_t ctx ) : so_5::agent_t( ctx ) { this >>= st_free; st_free.event( [this]( mhood_t<take_t> cmd ) { this >>= st_taken; so_5::send< taken_t >( cmd->m_who ); } ); st_taken.event( []( mhood_t<take_t> cmd ) { so_5::send< busy_t >( cmd->m_who ); } ) .just_switch_to< put_t >( st_free ); } private : const state_t st_free{ this }; const state_t st_taken{ this }; };
Aquí ni siquiera necesitamos mantener la cola de los filósofos que esperan.
Actor filósofo
El filósofo-actor en esta implementación es similar al de la solución de Dijkstra, pero aquí el filósofo-actor también tiene que procesar busy_t
, por lo que el diagrama de estado se ve así:

Del mismo modo, toda la lógica de un actor-filósofo se define en so_define_agent()
:
void so_define_agent() override { st_thinking .event< stop_thinking_t >( [=] { this >>= st_wait_left; so_5::send< take_t >( m_left_fork, so_direct_mbox(), m_index ); } ); st_wait_left .event< taken_t >( [=] { this >>= st_wait_right; so_5::send< take_t >( m_right_fork, so_direct_mbox(), m_index ); } ) .event< busy_t >( [=] { think( st_hungry_thinking ); } ); st_wait_right .event< taken_t >( [=] { this >>= st_eating; } ) .event< busy_t >( [=] { so_5::send< put_t >( m_left_fork ); think( st_hungry_thinking ); } ); st_eating .on_enter( [=] { so_5::send_delayed< stop_eating_t >( *this, eat_pause() ); } ) .event< stop_eating_t >( [=] { so_5::send< put_t >( m_right_fork ); so_5::send< put_t >( m_left_fork ); ++m_meals_eaten; if( m_meals_count == m_meals_eaten ) this >>= st_done; else think( st_normal_thinking ); } ); st_done .on_enter( [=] { completion_watcher_t::done( so_environment(), m_index ); } ); }
En general, este es casi el mismo código que en la solución de Dijkstra, a excepción de un par de controladores para busy_t
.
Resultados
Los resultados del trabajo se ven diferentes:
Socrates: tttttttttL..R.....EEEEEEEEEEEEttttttttttR...LL..EEEEEEEttEEEEEE Plato: ttttEEEEEEEEEEEttttttL.....L..EEEEEEEEEEEEEEEttttttttttL....L.... Aristotle: ttttttttttttL..LR.EEEEEEtttttttttttL..L....L....R.....EEEEEEEEE Descartes: ttttttttttEEEEEEEEttttttttttttEEEEEEEEttttEEEEEEEEEEEttttttL..L.. Spinoza: ttttttttttL.....L...EEEEEEtttttttttL.L......L....L..L...R...R...E Kant: tttttttEEEEEEEttttttttL.L.....EEEEEEEEttttttttR...R..R..EEEEEtttt Schopenhauer: tttR..R..L.....EEEEEEEttttttR.....L...EEEEEEEEEEEEEEEEttttttttttt Nietzsche: tttEEEEEEEEEEtttttttttEEEEEEEEEEEEEEEttttL....L...L..L....EEEEEEE Wittgenstein: tttttL.L..L.....RR....L.....L....L...EEEEEEEEEEEEEEEtttttttttL. Heidegger: ttttR..R......EEEEEEEEEEEEEttttttttttR..L...L...L..L...EEEEtttttt Sartre: tttEEEEEEEtttttttL..L...L....R.EEEEEEEtttttEEEEtttttttR.....R..R.
Aquí vemos un nuevo símbolo, lo que significa que el actor-filósofo está en "pensamientos hambrientos".
Incluso en este breve fragmento, uno puede ver que hay largos períodos de tiempo durante los cuales el filósofo no puede comer. Esto se debe a que esta solución está protegida del problema de punto muerto, pero no tiene protección contra el hambre.
La decisión con el camarero y la cola.
La solución más simple que se muestra arriba sin un árbitro no protege contra el hambre. El artículo "Filósofos gastronómicos modernos" mencionado anteriormente contiene una solución al problema del ayuno en forma de un protocolo WaiterFair. La conclusión es que hay un árbitro (camarero), al que recurren los filósofos cuando quieren comer. Y el camarero tiene una cola de solicitudes de filósofos. Y el filósofo obtiene los tenedores solo si ambos tenedores están libres ahora, y no hay ninguno de los vecinos del filósofo que recurrió al camarero en la cola.
Echemos un vistazo a cómo podría verse esta misma solución en los actores.
El código fuente de esta implementación se puede encontrar aquí .
Truco
La forma más fácil sería introducir un nuevo conjunto de mensajes a través del cual los filósofos pudieran comunicarse con el camarero. Pero quería guardar no solo el conjunto de mensajes ya existente (es decir, taken_t
, busy_t
, put_t
, put_t
). También quería que se utilizara el mismo actor-filósofo que en la solución anterior. Por lo tanto, tuve que resolver un problema complicado: cómo hacer que el actor-filósofo se comunique con el único actor-camarero, pero al mismo tiempo pensé que interactúa directamente con los tenedores de actores (que ya no están).
Este problema se resolvió mediante un simple truco: un actor-camarero crea un conjunto de mbox-s, enlaces a los filósofos-actores como enlaces a mbox-s de tenedores. Al mismo tiempo, el actor-camarero se suscribe a los mensajes de todos estos mboxes (que se implementa fácilmente en SObjectizer, porque SObjectizer es una implementación de no solo / tantos modelos de actores, sino que también Pub / Sub es compatible de fábrica) .
En código, se parece a esto:
class waiter_t final : public so_5::agent_t { public : waiter_t( context_t ctx, std::size_t forks_count ) : so_5::agent_t{ std::move(ctx) } , m_fork_states( forks_count, fork_state_t::free ) {
Es decir Primero, cree un vector de mbox-s para "bifurcaciones" inexistentes, luego suscríbase a cada una de ellas. Sí, nos suscribimos para saber a qué enchufe en particular se refiere la solicitud.
El controlador real para la on_take_fork()
entrante on_take_fork()
es el método on_take_fork()
:
void on_take_fork( mhood_t<take_t> cmd, std::size_t fork_index ) {
Por cierto, fue aquí donde necesitábamos el segundo campo del mensaje take_t
.
Entonces, en on_take_fork()
tenemos la solicitud original y el índice de la bifurcación a la que se refiere la solicitud. Por lo tanto, podemos determinar si el filósofo pide un tenedor izquierdo o uno derecho. Y, en consecuencia, podemos procesarlos de manera diferente (y tenemos que procesarlos de manera diferente).
Dado que el filósofo siempre pregunta primero por la bifurcación izquierda, entonces necesitamos hacer todas las verificaciones necesarias en este mismo momento. Y podemos encontrarnos en una de las siguientes situaciones:
- Ambos tenedores son gratuitos y se pueden entregar al filósofo que envió la solicitud. En este caso,
taken_t
filósofo y marcamos la bifurcación derecha como reservada para que nadie más pueda tomarla. - Los tenedores no se pueden dar al filósofo. No importa por qué Tal vez algunos de ellos están ocupados en este momento. O en línea es uno de los vecinos del filósofo. De todos modos, ponemos al filósofo que envió la solicitud en la cola, después de lo cual le
busy_t
.
Gracias a esta lógica de trabajo, el filósofo que recibió taken_t
para la bifurcación izquierda puede enviar de forma segura una solicitud take_t
para la bifurcación derecha. Esta solicitud será satisfecha de inmediato, ya que el tenedor ya está reservado para este filósofo.
Resultados
Si ejecuta la solución resultante, puede ver algo como:
Socrates: tttttttttttL....EEEEEEEEEEEEEEttttttttttL...L...EEEEEEEEEEEEEtttttL. Plato: tttttttttttL....L..L..L...L...EEEEEEEEEEEEEtttttL.....L....L.....EEE Aristotle: tttttttttL.....EEEEEEEEEttttttttttL.....L.....EEEEEEEEEEEtttL....LL Descartes: ttEEEEEEEEEEtttttttL.L..EEEEEEEEEEEEtttL..L....L....L.....EEEEEEEEEE Spinoza: tttttttttL.....EEEEEEEEEttttttttttL.....L.....EEEEEEEEEEEtttL....LL Kant: ttEEEEEEEEEEEEEtttttttL...L.....L.....EEEEEttttL....L...L..L...EEEEE Schopenhauer: ttttL...L.....L.EEEEEEEEEEEEEEEEEtttttttttttL..L...L..EEEEEEEttttttt Nietzsche: tttttttttttL....L..L..L...L...L.....L....EEEEEEEEEEEEttL.....L...L.. Wittgenstein: tttttttttL....L...L....L....L...EEEEEEEttttL......L.....L.....EEEEEE Heidegger: ttttttL..L...L.....EEEEEEEEEEEEtttttL...L..L.....EEEEEEEEEEEttttttL. Sartre: ttEEEEEEEEEEEEEttttttttL.....L...EEEEEEEEEEEEttttttttttttL.....EEEEE
Puedes prestar atención a la falta de caracteres R
Esto se debe a que las fallas o expectativas no pueden ocurrir en una solicitud de bifurcación correcta.
Otra decisión usando un árbitro (camarero)
En algunos casos, la solución waiter_with_queue anterior puede mostrar resultados similares a este:
Socrates: tttttEEEEEEEEEEEEEEtttL.....LL...L....EEEEEEEEEttttttttttL....L.....EE Plato: tttttL..L..L....LL...EEEEEEEEEEEEEEEttttttttttttL.....EEEEEEEEEttttttt Aristotle: tttttttttttL..L...L.....L.....L....L.....EEEEEEEEEEEEtttttttttttL....L.. Descartes: ttttttttttEEEEEEEEEEttttttL.....L....L..L.....L.....L..L...L..EEEEEEEEtt Spinoza: tttttttttttL..L...L.....L.....L....L.....L..L..L....EEEEEEEEEEtttttttttt Kant: tttttttttL....L....L...L...L....L..L...EEEEEEEEEEEttttttttttL...L......E Schopenhauer: ttttttL....L..L...L...LL...L...EEEEEtttttL....L...L.....EEEEEEEEEttttt Nietzsche: tttttL..L..L....EEEEEEEEEEEEEttttttttttttEEEEEEEEEEEEEEEttttttttttttL... Wittgenstein: tttEEEEEEEEEEEEtttL....L....L..EEEEEEEEEtttttL..L..L....EEEEEEEEEEEEEEEE Heidegger: tttttttttL...L..EEEEEEEEttttL..L.....L...EEEEEEEEEtttL.L..L...L....L...L Sartre: ttttttttttL..L....L...L.EEEEEEEEEEEtttttL...L..L....EEEEEEEEEEtttttttttt
Puede ver la presencia de períodos de tiempo suficientemente largos cuando los filósofos no pueden comer, incluso a pesar de la presencia de tenedores gratuitos. Por ejemplo, las horquillas izquierda y derecha de Kant son gratuitas durante mucho tiempo, pero Kant no puede tomarlas, porque sus vecinos ya están esperando en la fila. Que están esperando a sus vecinos. Quienes esperan a sus vecinos, etc.
Por lo tanto, la implementación de waiter_with_queue discutida anteriormente protege contra la inanición en el sentido de que tarde o temprano el filósofo comerá. Esto está garantizado para él. Pero los períodos de ayuno pueden ser bastante largos. Y la utilización de recursos puede no ser óptima a veces.
Para resolver este problema, implementé otra solución, waiter_with_timestamp (su código se puede encontrar aquí ). En lugar de hacer cola, priorizan las solicitudes de los filósofos teniendo en cuenta el momento de su ayuno. Cuanto más tiempo pase el filósofo hambriento, más prioridad tendrá su solicitud.
No consideraremos el código para esta solución, porque en general, lo principal es el mismo truco con un conjunto de mboxes para "bifurcaciones" inexistentes, que ya discutimos en la conversación sobre la implementación de waiter_with_queue.
Algunos detalles de implementación a los que me gustaría llamar la atención
Hay varios detalles en las implementaciones basadas en los actores a los que me gustaría prestar atención, porque Estos detalles demuestran características interesantes de SObjectizer.
Contexto laboral para actores
En las implementaciones consideradas, todos los actores principales ( fork_t
, philosopher_t
, waiter_t
) trabajaron en el contexto de un hilo de trabajo común. Lo que no significa en absoluto que en SObjectizer todos los actores trabajen en un solo hilo. En SObjectizer, puede vincular actores a diferentes contextos, que se pueden ver, por ejemplo, en el código de la función run_simulation()
en la solución no_waiter_simple.
Código de simulación de ejecución de no_waiter_simple void run_simulation( so_5::environment_t & env, const names_holder_t & names ) { env.introduce_coop( [&]( so_5::coop_t & coop ) { coop.make_agent_with_binder< trace_maker_t >( so_5::disp::one_thread::create_private_disp( env )->binder(), names, random_pause_generator_t::trace_step() ); coop.make_agent_with_binder< completion_watcher_t >( so_5::disp::one_thread::create_private_disp( env )->binder(), names ); const auto count = names.size(); std::vector< so_5::agent_t * > forks( count, nullptr ); for( std::size_t i{}; i != count; ++i ) forks[ i ] = coop.make_agent< fork_t >(); for( std::size_t i{}; i != count; ++i ) coop.make_agent< philosopher_t >( i, forks[ i ]->so_direct_mbox(), forks[ (i + 1) % count ]->so_direct_mbox(), default_meals_count ); }); }
En esta función, completion_watcher_t
actores adicionales de los tipos trace_maker_t
y completion_watcher_t
. Trabajarán en contextos de trabajo individuales. Para hacer esto, se one_thread
dos instancias del despachador del tipo one_thread
y los actores están vinculados a estas instancias de despachadores. Lo que significa que estos actores trabajarán como objetos activos : cada uno tendrá su propio hilo de trabajo.
SObjectizer proporciona un conjunto de varios despachadores diferentes que se pueden usar directamente. En este caso, el desarrollador puede crear en su aplicación tantas instancias de despachadores como necesite el desarrollador.
, , . , fork_t
, philosopher_t
.
run_simulation no_waiter_simple_tp void run_simulation( so_5::environment_t & env, const names_holder_t & names ) { env.introduce_coop( [&]( so_5::coop_t & coop ) { coop.make_agent_with_binder< trace_maker_t >( so_5::disp::one_thread::create_private_disp( env )->binder(), names, random_pause_generator_t::trace_step() ); coop.make_agent_with_binder< completion_watcher_t >( so_5::disp::one_thread::create_private_disp( env )->binder(), names ); const auto count = names.size();
fork_t
philosopher_t
.
Modern dining philosophers , , :
void doEat() { eventLog_.startActivity(ActivityType::eat); wait(randBetween(10, 50)); eventLog_.endActivity(ActivityType::eat);
SObjectizer . , , . ¿Por qué?
, SObjectizer- : . agent_state_listener_t
. , SObjectizer .
greedy_philosopher_t
philosopher_t
:
philosopher_t(...) ... { so_add_destroyable_listener( state_watcher_t::make( so_environment(), index ) ); }
state_watcher_t
— .
state_watcher_t class state_watcher_t final : public so_5::agent_state_listener_t { const so_5::mbox_t m_mbox; const std::size_t m_index; state_watcher_t( so_5::mbox_t mbox, std::size_t index ); public : static auto make( so_5::environment_t & env, std::size_t index ) { return so_5::agent_state_listener_unique_ptr_t{ new state_watcher_t{ trace_maker_t::make_mbox(env), index } }; } void changed( so_5::agent_t &, const so_5::state_t & state ) override; };
state_watcher_t
SObjectizer changed()
. state_watcher_t::changed
-.
state_watcher_t::changed void state_watcher_t::changed( so_5::agent_t &, const so_5::state_t & state ) { const auto detect_label = []( const std::string & name ) {...}; const char state_label = detect_label( state.query_name() ); if( '?' == state_label ) return; so_5::send< trace::state_changed_t >( m_mbox, m_index, state_label ); }
CSP
, . (no_waiter_dijkstra, no_waiter_simple, waiter_with_timestamps) std::thread
SObjectizer- mchain- (, , CSP- ). , , CSP- ( take_t
, taken_t
, busy_t
, put_t
).
CSP- "" . , std::thread
.
.
: + take_t
put_t
. fork_process
:
void fork_process( so_5::mchain_t fork_ch ) {
fork_process
: , - .
fork_process
— "" , . receive()
:
so_5::receive( so_5::from( fork_ch ), [&]( so_5::mhood_t<take_t> cmd ) {...}, [&]( so_5::mhood_t<put_t> ) {...} );
SObjectizer- receive()
. . . , . , .
-. fork_t
. , , .
philosopher_process
. , .
philosopher_process oid philosopher_process( trace_maker_t & tracer, so_5::mchain_t control_ch, std::size_t philosopher_index, so_5::mbox_t left_fork, so_5::mbox_t right_fork, int meals_count ) { int meals_eaten{ 0 }; random_pause_generator_t pause_generator;
:
void philosopher_process( trace_maker_t & tracer, so_5::mchain_t control_ch, std::size_t philosopher_index, so_5::mbox_t left_fork, so_5::mbox_t right_fork, int meals_count )
.
SObjectizer- , , Actor-. :
tracer.thinking_started( philosopher_index, thinking_type_t::normal );
tracer
, .
control_ch
, philosopher_done_t
, , . .
left_fork
right_fork
. take_t
put_t
. , mbox_t
mchain_t
?
! , . , mchain — - mbox-, mchain- mbox_t
.
, :
int meals_eaten{ 0 }; random_pause_generator_t pause_generator; auto self_ch = so_5::create_mchain( control_ch->environment() );
— self_ch
. , .
. Es decir , .
, , this_thread::sleep_for
.
, :
so_5::send< take_t >( left_fork, self_ch->as_mbox(), philosopher_index );
take_t
. mbox_t
, self_ch
mchain_t
. as_mbox()
.
receive()
:
so_5::receive( so_5::from( self_ch ).handle_n( 1u ), [&]( so_5::mhood_t<taken_t> ) {...} );
taken_t
. . , .
- , philosopher_process
. receive()
:
so_5::receive( so_5::from( self_ch ).handle_n( 1u ), [&]( so_5::mhood_t<taken_t> ) { ... so_5::receive( so_5::from( self_ch ).handle_n( 1u ), [&]( so_5::mhood_t<taken_t> ) {...} ); ... } );
- .
run_simulation()
, . CSP- run_simulation()
. , , ( ).
run_simulation void run_simulation( so_5::environment_t & env, const names_holder_t & names ) noexcept { const auto table_size = names.size(); const auto join_all = []( std::vector<std::thread> & threads ) { for( auto & t : threads ) t.join(); }; trace_maker_t tracer{ env, names, random_pause_generator_t::trace_step() };
, run_simulation()
- . .
. :
std::vector< so_5::mchain_t > fork_chains; std::vector< std::thread > fork_threads( table_size ); for( std::size_t i{}; i != table_size; ++i ) { fork_chains.emplace_back( so_5::create_mchain(env) ); fork_threads[ i ] = std::thread{ fork_process, fork_chains.back() }; }
, , . . , join
.
, .. join
:
std::vector< std::thread > philosopher_threads( table_size ); for( std::size_t i{}; i != table_size - 1u; ++i ) { philosopher_threads[ i ] = philosopher_maker( i, i, i+1u ); } philosopher_threads[ table_size - 1u ] = philosopher_maker( table_size - 1u, table_size - 1u, 0u );
. :
so_5::receive( so_5::from( control_ch ).handle_n( table_size ), [&names]( so_5::mhood_t<philosopher_done_t> cmd ) { fmt::print( "{}: done\n", names[ cmd->m_philosopher_index ] ); } );
receive()
table_size
philosopher_done_t
.
philosopher_done_t
.
join
:
join_all( philosopher_threads );
join
. join
, .. . receive()
. join
:
for( auto & ch : fork_chains ) so_5::close_drop_content( ch ); join_all( fork_threads );
.
noexcept
, run_simulation
, noexcept . , exception-safety . — .
run_simulation
?
, . - exception-safety , . - , :
try { for( std::size_t i{}; i != table_size; ++i ) { fork_chains.emplace_back( so_5::create_mchain(env) ); fork_threads[ i ] = std::thread{ fork_process, fork_chains.back() }; } } catch( ... ) { for( std::size_t i{}; i != fork_chains.size(); ++i ) { so_5::close_drop_content( fork_chains[ i ] ); if( fork_threads[ i ].joinable() ) fork_threads[ i ].join(); } throw; }
, . Porque , . - :
struct fork_threads_stuff_t { std::vector< so_5::mchain_t > m_fork_chains; std::vector< std::thread > m_fork_threads; fork_threads_stuff_t( std::size_t table_size ) : m_fork_threads( table_size ) {} ~fork_threads_stuff_t() { for( std::size_t i{}; i != m_fork_chains.size(); ++i ) { so_5::close_drop_content( m_fork_chains[ i ] ); if( m_fork_threads[ i ].joinable() ) m_fork_threads[ i ].join(); } } void run() { for( std::size_t i{}; i != m_fork_threads.size(); ++i ) { m_fork_chains.emplace_back( so_5::create_mchain(env) ); m_fork_threads[ i ] = std::thread{ fork_process, m_fork_chains.back() }; } } } fork_threads_stuff{ table_size };
, (, Boost- ScopeExit-, GSL- finally() ).
. .
, exception-safety run_simulation()
, run_simulation()
, . , -. exception-safety run_simulation()
noexcept , std::terminate
. , .
, , , , . , join
, join
. .
()
, CSP- , .
.
fork_process
, :
void fork_process( so_5::mchain_t fork_ch ) {
, fork_process
, ( , ).
philosopher_process
, , .
philosopher_process void philosopher_process( trace_maker_t & tracer, so_5::mchain_t control_ch, std::size_t philosopher_index, so_5::mbox_t left_fork, so_5::mbox_t right_fork, int meals_count ) { int meals_eaten{ 0 };
- philosopher_process
philosopher_process
. .
-, thinking_type
. , , , "" .
-, busy_t
. receive()
:
so_5::receive( so_5::from( self_ch ).handle_n( 1u ), []( so_5::mhood_t<busy_t> ) { }, [&]( so_5::mhood_t<taken_t> ) { ... so_5::receive( so_5::from( self_ch ).handle_n( 1u ), []( so_5::mhood_t<busy_t> ) { }, [&]( so_5::mhood_t<taken_t> ) {...} );
, busy_t
, , receive()
, receive()
. busy_t
. , .. receive()
busy_t
. receive()
busy_t
.
CSP- , . (): waiter_with_queue, , waiter_with_timestamps. : mbox- , mbox- , mbox- .
CSP- , philosopher_process
no_waiter_simple. mchain- , ?
, .
mchain- . , mchain-.
SObjectizer- select()
, , , :
so_5::select( so_5::from_all(), case_(ch1, one_handler_1, one_handler_2, one_handler_3, ...), case_(ch2, two_handler_1, two_handler_2, two_handler_3, ...), ...);
select()
, -. " " . CSP- .
.
, , take_t
put_t
. - . take_t
put_t
, :
struct extended_take_t final : public so_5::message_t { const so_5::mbox_t m_who; const std::size_t m_philosopher_index; const std::size_t m_fork_index; extended_take_t( so_5::mbox_t who, std::size_t philosopher_index, std::size_t fork_index ) : m_who{ std::move(who) } , m_philosopher_index{ philosopher_index } , m_fork_index{ fork_index } {} }; struct extended_put_t final : public so_5::message_t { const std::size_t m_fork_index; extended_put_t( std::size_t fork_index ) : m_fork_index{ fork_index } {} };
, so_5::message_t
, ( ). , SObjectizer- .
, . take_t
put_t
, extended_take_t
extended_put_t
, .
mbox. :)
mbox- class wrapping_mbox_t final : public so_5::extra::mboxes::proxy::simple_t { using base_type_t = so_5::extra::mboxes::proxy::simple_t;
mbox-: so_5_extra , . so_5::abstract_message_box_t
.
, wrapping_mbox_t
. , . wrapping_mbox, mchain . waiter_process
, , :
void waiter_process( so_5::mchain_t waiter_ch, details::waiter_logic_t & logic ) {
, , . waiter_with_timestamps .
: " philosopher_process
mbox-?" , waiter_with_timestamps mbox, mchain.
, mchain. , .. so_5_extra mchain- ( ). mbox- mchain-.
Conclusión
, , , CSP . , . , , . , - .
, SObjectizer-. , "" SObjectizer — 5.6, 5.5. , ( ). - , SO-5.6 ( ).
, !
PS. "" , . C++14.