Há algum tempo, um link para o artigo "Modernos filósofos gastronômicos" se espalhou por recursos como Reddit e HackerNews. O artigo é interessante, mostra várias soluções para essa tarefa bem conhecida, implementada no C ++ moderno usando uma abordagem baseada em tarefas. Se alguém ainda não leu este artigo, faz sentido gastar tempo e lê-lo.
No entanto, não posso dizer que as soluções apresentadas no artigo pareciam simples e compreensíveis para mim. Isso provavelmente se deve ao uso de tarefas. Muitos deles são criados e despachados através de uma variedade de expedidores / serializadores. Portanto, nem sempre é claro onde, quando e quais tarefas são executadas.
Além disso, a abordagem baseada em tarefas não é a única possível para resolver esses problemas. Por que não ver como a tarefa dos "filósofos do jantar" é resolvida através dos modelos de Atores e CSP?
Portanto, tentei procurar e implementar várias soluções para esse problema usando Atores e CSP. O código para essas soluções pode ser encontrado no repositório no GitHub . E sob o cortador, explicações e explicações, para quem estiver interessado, seja bem-vindo.
Algumas palavras comuns
Eu não tinha o objetivo de repetir exatamente as decisões mostradas no próprio artigo "Filósofos modernos de restaurantes" , especialmente porque eu não gosto de nada de fundamental: na verdade, o filósofo não faz nada com essas decisões. Ele apenas diz "eu quero comer", e então alguém lhe dá garfos magicamente, ou ele diz "agora não vai funcionar".
Fica claro por que o autor recorreu a esse comportamento: permite o uso da mesma implementação do “filósofo” em conjunto com diferentes implementações dos “protocolos”. No entanto, parece-me pessoalmente que é mais interessante quando o "filósofo" tenta desconectar um primeiro e depois outro. E quando o "filósofo" é forçado a lidar com tentativas frustradas de capturar os garfos.
É precisamente essas realizações da tarefa dos "filósofos do jantar" que tentei realizar. Ao mesmo tempo, algumas soluções usaram as mesmas abordagens do artigo mencionado (por exemplo, implementadas pelos protocolos ForkLevelPhilosopherProtocol e WaiterFair).
Eu construí minhas decisões com base no SObjectizer , o que dificilmente surpreenderá quem já leu meus artigos antes. Se alguém ainda não ouviu falar do SObjectizer, em poucas palavras: esta é uma das poucas "estruturas de ator" ao vivo e em desenvolvimento do OpenSource para C ++ (o CAF e o QP / C ++ também podem ser mencionados, entre outros). Espero que os exemplos acima com meus comentários sejam suficientemente claros, mesmo para aqueles que não estão familiarizados com o SObjectizer. Caso contrário, terei prazer em responder às perguntas nos comentários.
Soluções para Atores
Começaremos a discussão das soluções implementadas com as baseadas nos Atores. Primeiro, considere a implementação da solução Edsger Dijkstra, depois passe para várias outras soluções e veja como o comportamento de cada uma das soluções difere.
Decisão de Dijkstra
Edsger Dijkstra, ele não apenas formulou a tarefa de “comer filofos” (a formulação usando “garfos” e “espaguete” foi dublado por Tony Hoar), mas também propôs uma solução muito simples e bonita. A saber: os filósofos só devem pegar os garfos para aumentar o número de garfos, e se o filósofo conseguiu pegar o primeiro garfo, ele não o largará até receber o segundo garfo.
Por exemplo, se um filósofo precisa usar garfos com os números 5 e 6, então um filósofo deve primeiro pegar um garfo do número 5. Somente então ele pode pegar um garfo do número 6. Assim, se os garfos com números mais baixos estiverem à esquerda dos filósofos, o filósofo deve primeiro pegue o garfo esquerdo e só então ele poderá pegar o garfo direito.
O último filósofo da lista, que precisa lidar com os garfos nos números (N-1) e 0, faz o oposto: ele primeiro pega a bifurcação direita com o número 0 e depois a bifurcação esquerda com o número (N-1).
Para implementar essa abordagem, serão necessários dois tipos de atores: um para garfos e outro para filósofos. Se o filósofo quiser comer, ele envia uma mensagem ao ator de forquilha correspondente para capturá-lo, e o ator de forquilha responde com uma mensagem de resposta.
O código para implementar essa abordagem pode ser visto aqui .
Mensagens
Antes de falar sobre atores, você precisa observar as mensagens que os atores trocarão:
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 {};
Quando o ator-filósofo deseja desligar, ele envia a mensagem take_t
para o fork fork, e o fork fork responde com a mensagem taken_t
. Quando o ator-filósofo termina de comer e quer colocar os garfos de volta na mesa, ele envia mensagens de put_t ao put_t
garfos.
Na mensagem take_t
, o campo take_t
indica a caixa de correio (aka mbox) do ator filósofo. Uma mensagem de resposta taken_t
deve ser enviada para esta taken_t
. O segundo campo de take_t
não é usado neste exemplo, precisamos dele quando chegarmos às implementações de waiter_with_queue e waiter_with_timestamps.
Garfo de ator
Agora podemos ver o que é um ator de fork. Aqui está o código dele:
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 ator no SObjectizer deve ser derivado da classe base agent_t
. O que vemos aqui para o tipo fork_t
.
O método so_define_agent()
é substituído na classe so_define_agent()
. Este é um método especial, chamado automaticamente pelo SObjectizer ao registrar um novo agente. No método so_define_agent()
, o so_define_agent()
é "configurado" para funcionar no SObjectizer: o estado inicial é alterado, as mensagens necessárias são assinadas.
Cada ator no SObjectizer é uma máquina de estados com estados (mesmo que um ator use apenas um estado padrão). O ator fork_t
possui dois estados: livre e obtido . Quando um ator está em um estado livre , o plug pode ser "capturado" pelo filósofo. E depois de capturar o "fork", o ator fork_t
deve entrar no estado tomado . Dentro da classe fork_t
estados são representados por instâncias de st_free
e st_taken
tipo especial state_t
.
Os estados permitem processar mensagens recebidas de diferentes maneiras. Por exemplo, no estado livre , o agente responde apenas a take_t
e essa reação é muito simples: o estado do ator muda e a resposta taken_t
:
st_free .event( [this]( mhood_t<take_t> cmd ) { this >>= st_taken; so_5::send< taken_t >( cmd->m_who ); } );
Enquanto todas as outras mensagens, incluindo put_t
no estado livre , são simplesmente ignoradas.
No estado tomado , o ator processa duas mensagens e, mesmo a mensagem take_t
processa de maneira 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 ); } } );
O manipulador para put_t
mais interessante put_t
: se a fila de filósofos em espera estiver vazia, podemos voltar a liberar , mas se não estiver vazia, o primeiro deles precisará ser enviado como taken_t
.
Ator filósofo
O código do ator-filósofo é muito mais volumoso, por isso não o darei aqui na íntegra. Discutiremos apenas os fragmentos mais significativos.
Um ator-filósofo tem um pouco mais 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" };
O ator começa seu trabalho em um estado de pensamento , depois muda para wait_left , depois wait_right e depois para comer . Ao comer, um ator pode voltar a pensar ou pode terminar se o filósofo comeu tudo o que deveria.
O diagrama de estado para um ator-filósofo pode ser representado da seguinte maneira:

A lógica do comportamento do ator é descrita na implementação de seu método so_define_agent()
:
void so_define_agent() override {
Talvez o único ponto que deva ser particularmente enfatizado seja a abordagem para imitar os processos de "pensar" e "comer". Não há this_thread::sleep_for
no código do ator ou qualquer outra maneira de bloquear o segmento de trabalho atual. Em vez disso, as mensagens pendentes são usadas. Por exemplo, quando um ator entra no estado alimentar , ele envia para si uma mensagem pendente stop_eating_t
. Esta mensagem é dada ao cronômetro do SObjectizer e o cronômetro entrega a mensagem ao ator quando chegar a hora.
O uso de mensagens atrasadas permite executar todos os atores no contexto de um único encadeamento de trabalho. Grosso modo, um encadeamento lê mensagens de alguma fila e arranca o próximo manipulador de mensagens do ator destinatário correspondente. Mais sobre contextos de trabalho para atores serão discutidos abaixo.
Resultados
Os resultados desta implementação podem ter a seguinte aparência (um pequeno fragmento):
Socrates: tttttttttttLRRRRRRRRRRRRRREEEEEEEttttttttLRRRRRRRRRRRRRREEEEEEEEEEEEE Plato: ttttttttttEEEEEEEEEEEEEEEEttttttttttRRRRRREEEEEEEEEEEEEEttttttttttLLL Aristotle: ttttEEEEEtttttttttttLLLLLLRRRREEEEEEEEEEEEttttttttttttLLEEEEEEEEEEEEE Descartes: tttttLLLLRRRRRRRREEEEEEEEEEEEEtttLLLLLLLLLRRRRREEEEEEttttttttttLLLLLL Spinoza: ttttEEEEEEEEEEEEEttttttttttLLLRRRREEEEEEEEEEEEEttttttttttRRRREEEEEEtt Kant: ttttttttttLLLLLLLRREEEEEEEEEEEEEEEttttttttttLLLEEEEEEEEEEEEEEtttttttt Schopenhauer: ttttttEEEEEEEEEEEEEttttttLLLLLLLLLEEEEEEEEEttttttttLLLLLLLLLLRRRRRRRR Nietzsche: tttttttttLLLLLLLLLLEEEEEEEEEEEEEttttttttLLLEEEEEEEEEttttttttRRRRRRRRE Wittgenstein: ttttEEEEEEEEEEtttttLLLLLLLLLLLLLEEEEEEEEEttttttttttttRRRREEEEEEEEEEEt Heidegger: tttttttttttLLLEEEEEEEEEEEEEEtttttttLLLLLLREEEEEEEEEEEEEEEtttLLLLLLLLR Sartre: tttEEEEEEEEEttttLLLLLLLLLLLLRRRRREEEEEEEEEtttttttLLLLLLLLRRRRRRRRRRRR
Leia isto da seguinte maneira:
t
denota que o filósofo está "pensando";L
significa que o filósofo espera capturar o garfo esquerdo (no estado wait_left )R
significa que o filósofo espera capturar a bifurcação correta (está no estado wait_right );E
significa que o filósofo "come".
Podemos ver que Sócrates pode pegar o garfo à esquerda somente depois que Sartre o der. Após o que Sócrates esperará até Platão soltar o garfo certo. Só depois Sócrates poderá comer.
Uma decisão simples sem um árbitro (garçom)
Se analisarmos o resultado da decisão de Dijkstra, veremos que os filósofos passam muito tempo esperando a captura dos garfos. O que não é bom, porque esse tempo também pode ser gasto em reflexão. Não é à toa que existe uma opinião de que, se você pensar com o estômago vazio, poderá obter resultados muito mais interessantes e inesperados;)
Vejamos a solução mais simples, na qual o filósofo retorna o primeiro garfo capturado se não puder capturar o segundo (no artigo "Filósofos modernos de restaurantes" mencionado acima, essa solução é implementada pelo ForkLevelPhilosopherProtocol).
O código fonte desta implementação pode ser visto aqui e o código do ator filósofo correspondente aqui .
Mensagens
Esta solução usa quase o mesmo conjunto de mensagens:
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 {};
A única diferença é a presença do sinal busy_t
. O ator-garfo envia esse sinal em resposta ao filósofo-ator se o garfo já foi capturado por outro filósofo.
Garfo de ator
O ator de garfo nesta solução é ainda mais simples do que na solução da 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 }; };
Aqui nem precisamos manter a fila de filósofos que aguardam.
Ator filósofo
O filósofo-ator nesta implementação é semelhante ao da solução de Dijkstra, mas aqui o filósofo-ator também precisa processar busy_t
, de modo que o diagrama de estado se parece com o seguinte:

Da mesma forma, toda a lógica de um ator-filósofo é definida em 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 ); } ); }
Em geral, esse é quase o mesmo código da solução da Dijkstra, exceto alguns manipuladores para busy_t
.
Resultados
Os resultados do trabalho parecem 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.
Aqui vemos um novo símbolo, o que significa que o ator-filósofo está em "pensamentos famintos".
Mesmo neste pequeno fragmento, pode-se ver que há longos períodos de tempo durante os quais o filósofo não pode comer. Isso ocorre porque esta solução está protegida do problema de conflito, mas não possui proteção contra a fome.
A decisão com o garçom e a fila
A solução mais simples mostrada acima sem um árbitro não protege contra a fome. O artigo "Filósofos modernos de restaurantes" mencionado acima contém uma solução para o problema do jejum na forma de um protocolo WaiterFair. A conclusão é que existe um árbitro (garçom), ao qual os filósofos recorrem quando querem comer. E o garçom tem uma fila de pedidos de filósofos. E o filósofo só consegue os garfos se os dois estiverem livres agora, e não há nenhum vizinho do filósofo que se voltou para o garçom na fila.
Vamos dar uma olhada em como essa mesma solução pode aparecer nos atores.
O código fonte desta implementação pode ser encontrado aqui .
Truque
A maneira mais fácil seria introduzir um novo conjunto de mensagens através do qual os filósofos pudessem se comunicar com o garçom. Mas eu queria salvar não apenas o conjunto de mensagens já existente (por exemplo, taken_t
, busy_t
, put_t
, put_t
). Eu também queria que o mesmo ator-filósofo fosse usado como na solução anterior. Portanto, tive que resolver um problema complicado: como fazer o ator-filósofo se comunicar com o único ator-garçom, mas ao mesmo tempo pensei que ele interage diretamente com os atores-garfos (que já se foram).
Esse problema foi resolvido usando um truque simples: um ator-garçom cria um conjunto de mbox-s, links aos quais são dados aos filósofos-atores como links para atores-mbox de plugues. Ao mesmo tempo, o ator-garçom se inscreve nas mensagens de todas essas mboxes (o que é facilmente implementado no SObjectizer, porque o SObjectizer é uma implementação não apenas / do maior número de modelos de atores, mas também de suporte a publicação / publicação) .
No código, parece algo como isto:
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 ) {
I.e. Primeiro, crie um vetor de mbox-s para "garfos" inexistentes e depois assine cada um deles. Sim, assinamos para saber a qual plug específico o pedido se refere.
O manipulador real da solicitação de entrada on_take_fork()
é o método on_take_fork()
:
void on_take_fork( mhood_t<take_t> cmd, std::size_t fork_index ) {
A propósito, foi aqui que precisávamos do segundo campo da mensagem take_t
.
Portanto, em on_take_fork()
, temos a solicitação original e o índice da bifurcação à qual a solicitação está relacionada. Portanto, podemos determinar se o filósofo pede um garfo esquerdo ou direito. E, portanto, podemos processá-los de maneira diferente (e precisamos processá-los de maneira diferente).
Como o filósofo sempre pede primeiro o garfo esquerdo, precisamos fazer todas as verificações necessárias neste exato momento. E podemos nos encontrar em uma das seguintes situações:
- Ambos os garfos são gratuitos e podem ser dados ao filósofo que enviou o pedido. Nesse caso,
taken_t
filósofo e marcamos o garfo direito como reservado, para que ninguém mais possa pegá-lo. - Forks não pode ser dado ao filósofo. Não importa o porquê. Talvez alguns deles estejam ocupados agora. Ou na fila é um dos vizinhos do filósofo. Enfim, colocamos o filósofo que enviou a solicitação na fila, após o que
busy_t
ele.
Graças a essa lógica de trabalho, o filósofo que recebeu o taken_t
para a bifurcação esquerda pode enviar com segurança um pedido de take_t
para a bifurcação direita. Este pedido será imediatamente satisfeito, como o garfo já está reservado para esse filósofo.
Resultados
Se você executar a solução resultante, poderá 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
Você pode prestar atenção à falta de caracteres R
Isso ocorre porque falhas ou expectativas não podem ocorrer em uma solicitação de bifurcação correta.
Outra decisão usando um árbitro (garçom)
Em alguns casos, a solução waiter_with_queue anterior pode mostrar resultados semelhantes 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
Você pode ver a presença de períodos suficientemente longos em que os filósofos não podem comer, apesar da presença de garfos gratuitos. Por exemplo, os garfos esquerdo e direito de Kant ficam livres por um longo tempo, mas Kant não pode pegá-los, porque seus vizinhos já estão esperando na fila. Que estão esperando por seus vizinhos. Quem está esperando seus vizinhos, etc.
Portanto, a implementação de waiter_with_queue discutida acima protege contra a fome no sentido de que mais cedo ou mais tarde o filósofo comerá. Isso é garantido para ele. Mas os períodos de jejum podem ser bastante longos. E a utilização de recursos pode não ser ideal às vezes.
Para resolver esse problema, implementei outra solução, waiter_with_timestamp (seu código pode ser encontrado aqui ). Em vez de fazer fila, eles priorizam solicitações de filósofos, levando em consideração o tempo de seu jejum. Quanto mais o filósofo passa fome, mais prioriza seu pedido.
Não consideraremos o código para esta solução, porque em geral, o principal é o mesmo truque com um conjunto de mboxes para "garfos" inexistentes, que já discutimos na conversa sobre a implementação de waiter_with_queue.
Alguns detalhes de implementação que gostaria de chamar a atenção
Existem vários detalhes nas implementações baseadas nos atores que eu gostaria de prestar atenção, porque esses detalhes demonstram recursos interessantes do SObjectizer.
Contexto de trabalho para atores
Nas implementações consideradas, todos os principais atores ( fork_t
, philosopher_t
, waiter_t
) trabalharam no contexto de um thread de trabalho comum. O que não significa que no SObjectizer todos os atores trabalhem em apenas um único thread. No SObjectizer, você pode vincular atores a diferentes contextos, o que pode ser visto, por exemplo, no código de função run_simulation()
na solução no_waiter_simple.
Código Run_simulation 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 ); }); }
Nesta função, atores adicionais dos tipos trace_maker_t
e completion_watcher_t
. Eles trabalharão em contextos de trabalho individuais. Para fazer isso, duas instâncias do expedidor do tipo one_thread
são one_thread
e os atores são vinculados a essas instâncias dos expedidores. O que significa que esses atores funcionarão como objetos ativos : cada um terá seu próprio segmento de trabalho.
O SObjectizer fornece um conjunto de vários despachantes diferentes que podem ser usados imediatamente. Nesse caso, o desenvolvedor pode criar em seu aplicativo quantas instâncias de expedidores quantas forem necessárias.
, , . , 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 . , , . Devido a 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
. , .
. I.e. , .
, , 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-.
Conclusão
, , , CSP . , . , , . , - .
, SObjectizer-. , "" SObjectizer — 5.6, 5.5. , ( ). - , SO-5.6 ( ).
, !
PS. "" , . C++14.