
Algumas palavras sobre o SObjectizer e sua história
O SObjectizer é uma estrutura C ++ bastante pequena que simplifica o desenvolvimento de aplicativos multithread. O SObjectizer permite que um desenvolvedor use abordagens dos modelos Actor, Publish-Subscribe e Communicating Sequential Processes (CSP). É um projeto OpenSource que é distribuído sob a licença BSD-3-CLAUSE.
O SObjectizer tem uma longa história. O SObjectizer nasceu em 2002 como projeto SObjectizer-4. Mas foi baseado em idéias do SCADA Objectizer anterior, desenvolvido entre 1995 e 2000. O SObjectizer-4 foi de código aberto em 2006, mas sua evolução foi interrompida logo depois. Uma nova versão do SObjectizer com o nome SObjectizer-5 foi iniciada em 2010 e de código aberto em 2013. A evolução do SObjectizer-5 ainda está em andamento e o SObjectizer-5 incorporou muitos novos recursos desde 2013.
O SObjectizer é mais ou menos conhecido no segmento russo da Internet, mas quase desconhecido fora do exUSSR. É porque o SObjectizer foi usado principalmente para projetos locais nos países exUSSR e muitos artigos, apresentações e palestras sobre o SObjectizer estão em russo.
O multithreading é usado na computação paralela e na computação simultânea . Mas há uma grande diferença entre computação paralela e simultânea. E, como conseqüência, existem ferramentas direcionadas à computação paralela, e existem ferramentas para computação simultânea, e elas são diferentes.
Grosso modo, a computação paralela é sobre o uso de vários núcleos para reduzir o tempo de cálculo. Por exemplo, a transcodificação de um arquivo de vídeo de um formato para outro pode levar uma hora em um núcleo da CPU, mas apenas 15 minutos em quatro núcleos da CPU. Ferramentas como OpenMP, Intel TBB, HPX ou cpp-taskflow foram projetadas para serem usadas na computação paralela. E essas ferramentas oferecem suporte apropriado para abordagens dessa área, como programação baseada em tarefas ou fluxo de dados.
A computação simultânea trata de lidar com muitas tarefas (provavelmente diferentes) ao mesmo tempo. O servidor de banco de dados ou o MQ-broker podem ser bons exemplos: um servidor precisa aceitar uma conexão, ler e analisar dados de conexões aceitas, manipular solicitações recebidas (executando várias ações para cada solicitação), enviando respostas e assim por diante. A rigor, não há necessidade de usar multithreading na computação simultânea: todas essas tarefas podem ser executadas em apenas um thread de trabalho. Mas o uso de multithreading e vários núcleos de CPU pode tornar seu aplicativo mais eficiente, escalável e responsivo.
Abordagens como o modelo do ator ou o CSP destinam-se a lidar com a computação simultânea. Bons exemplos de uso Os atores na área de computação simultânea são o projeto InfineSQL e o Yandex Message-Queue . Ambos os projetos usam atores internos.
Portanto, ferramentas como SObjectizer, QP / C ++ ou CAF, que suportam o Actor Model, são úteis na resolução de tarefas da área de computação simultânea. Isso significa que o uso do SObjectizer provavelmente não fornecerá nada em tarefas como a conversão de fluxos de vídeo. Mas você pode obter um resultado muito diferente implementando um intermediário de mensagens no SObjectizer.
Isenção de responsabilidade
O uso de modelos de ator ou CSP pode oferecer enormes benefícios em algumas tarefas, mas não há garantias de que esses modelos sejam apropriados para o seu problema específico. A discussão sobre a aplicabilidade dos modelos de ator ou CSP está além do escopo desse artigo. Vamos supor que o modelo de ator ou / e CSP seja aplicável às suas tarefas e você saiba como usá-las com eficiência.
O SObjectizer pode dar a um usuário?
Princípios de nada compartilhado e de ignorar apenas fora da caixa
O uso de atores pressupõe a ausência de quaisquer dados compartilhados. Todo ator possui seus dados e esses dados não são visíveis para mais ninguém. Esse é um princípio de nada compartilhado que é bem conhecido no desenvolvimento de aplicativos distribuídos, por exemplo. No aplicativo multithread, o princípio de compartilhamento de nada tem um benefício importante: permite evitar problemas perigosos para trabalhar com dados compartilhados, como deadlocks e data-races.
A interação entre atores (agentes) no SObjectizer é realizada apenas através de mensagens assíncronas. Um agente envia uma mensagem para outro agente e essa operação não bloqueia o remetente (em um caso comum).
A interação assíncrona permite usar outro princípio útil: disparar e esquecer . Quando um agente precisa de alguma operação, ele envia (dispara) uma mensagem e continua seu trabalho. Na maioria dos casos, a mensagem será recebida e processada.
Por exemplo, pode haver um agente que lê as conexões aceitas e analisa os dados recebidos. Se toda a PDU for lida e analisada, o agente envia a PDU para outro agente-processador e volta a ler / analisar novos dados recebidos.
Despachantes
Os expedidores são uma das pedras angulares do SObjectizer. Os expedidores fornecem um contexto de trabalho (também conhecido como thread de trabalho) no qual um agente manipula as mensagens recebidas. Em vez de criar threads de trabalho (ou conjuntos de threads) manualmente, um usuário cria expedidores e vincula agentes a eles. Um usuário pode criar quantos expedidores em um aplicativo quiser.
A melhor coisa com despachantes e agentes no SObjectizer é a separação de conceitos: despachantes são responsáveis por gerenciar o contexto de trabalho e as próprias filas de mensagens, os agentes executam a lógica do aplicativo e não se preocupam com o contexto do trabalhador. Permite mover um agente de um despachante para outro literalmente com um clique. Ontem, um agente trabalhou no despachante one_thread, hoje podemos reconectá-lo ao despachante active_obj e amanhã podemos reconfigurá-lo ao despachante thread_pool. Sem alterar uma linha na implementação do agente.
Existem oito tipos de despachantes no SObjectizer-5.6.0 (e outro pode ser encontrado no projeto complementar so5extra): começando de muito simples (one_thread ou thread_pool) a sofisticados (como adv_thread_pool ou prio_dedicated_threads :: one_per_prio). E um usuário pode escrever seu próprio despachante para condições específicas.
Máquinas de estado hierárquicas são funcionalidade incorporada
Os agentes (atores) no SObjectizer são máquinas de estado: a reação em uma mensagem recebida depende do estado atual do agente. O SObjectizer suporta a maioria dos recursos de máquinas de estado hierárquico (HSM): estados aninhados, histórico profundo e raso de um estado, manipuladores on_enter / on_exit, limites de tempo para permanecer em um estado. Somente estados ortogonais não são suportados no SObjectizer agora (não vimos a necessidade desse recurso em nossos projetos e ninguém nos pediu para adicionar suporte a esse recurso).
Canais do tipo CSP prontos para uso
Não há necessidade de usar agentes do SObjectizer (também conhecidos como atores). Todo o aplicativo pode ser desenvolvido usando apenas objetos std::thread
e mchains do SObjectizer (também conhecidos como canais CSP) . Nesse caso, o desenvolvimento de aplicativos com o SObjectizer será um pouco semelhante ao desenvolvimento na linguagem Go (incluindo um analógico da construção de select
Go que permite aguardar mensagens de vários canais).
As mchains do SObjectizer podem ter uma característica muito importante: mecanismo incorporado de contrapressão. Se um usuário criar um mchain com tamanho limitado e tentar enviar uma mensagem para o mchain completo, a operação de envio poderá bloquear o remetente por algum tempo. Permite resolver um problema famoso com um produtor rápido e um consumidor lento.
Os mchains do SObjectizer têm outra característica interessante: um mchain pode ser usado como uma ferramenta de distribuição de carga muito simples. Vários threads podem esperar o recebimento do mesmo mchain ao mesmo tempo. Se uma nova mensagem for enviada para esse mchain, apenas um thread lerá e manipulará essa mensagem.
Somente uma parte de um aplicativo pode usar o SObjectizer
Não há necessidade de usar o SObjectizer em todas as partes de um aplicativo. Apenas uma parte de um aplicativo pode ser desenvolvida usando o SObjectizer. Portanto, se você já usa Qt ou wxWidgets ou Boost.Asio como a estrutura principal do seu aplicativo, é possível usar o SObjectize em apenas um submódulo do seu aplicativo.
Tivemos experiência no uso do SObjectizer para desenvolvimento de bibliotecas que ocultam o uso do SObjectizer como um detalhe de implementação. A API pública dessas bibliotecas não expôs a presença de SObjectizer. O SObjectizer estava totalmente sob o controle de uma biblioteca: a biblioteca iniciou e parou o SObjectizer conforme necessário. Essas bibliotecas foram usadas em aplicativos que desconheciam completamente a presença do SObjectizer.
Se o SObjectizer for usado apenas em uma parte de um aplicativo, haverá uma tarefa de comunicação entre as partes do SObjectizer e não-SObjectizer do aplicativo. Esta tarefa é facilmente resolvida: as mensagens de uma parte que não seja do SObjectizer para a parte do SObjectizer podem ser enviadas através do mecanismo comum do SObjectizer de entrega de mensagens. Mensagens na direção oposta podem ser entregues via mchains.
Você pode executar várias instâncias do SObjectizer ao mesmo tempo
O SObjectizer permite executar várias instâncias do SObjectizer (chamado SObjectizer Environment) em um aplicativo ao mesmo tempo. Todo ambiente SObjectizer será independente de outros ambientes.
Esse recurso é inestimável em situações em que você precisa criar um aplicativo a partir de vários módulos independentes. Alguns módulos podem usar o SObjectizer, outros não. Os módulos que requerem o SObjectizer podem executar sua cópia do SObjectizer Environment e não terão influência em outros módulos no aplicativo.
Os temporizadores fazem parte do SObjectizer
O suporte de cronômetros na forma de mensagens atrasadas e periódicas é outro dos pilares do SObjectizer. O SObjectizer possui várias implementações de mecanismos de timer (timer_wheel, timer_heap e timer_list) e pode lidar com dezenas, centenas e milhares de milhões de timers em um aplicativo. Um usuário pode escolher o mecanismo de timer mais apropriado para um aplicativo. Além disso, um usuário pode fornecer sua própria implementação de timer_thread / timer_manager se nenhum dos padrões for apropriado para as condições do usuário.
O SObjectizer possui vários pontos de personalização e opções de ajuste
O SObjectizer permite a personalização de vários mecanismos importantes. Por exemplo, um usuário pode selecionar uma das implementações padrão de timer_thread (ou timer_manager). Ou pode fornecer sua própria implementação. Um usuário pode selecionar uma implementação de objetos de bloqueio usados por filas de mensagens nos despachantes do SObjectizer. Ou pode fornecer sua própria implementação.
Um usuário pode implementar seu próprio despachante. Um usuário pode implementar sua própria caixa de mensagem. Um usuário pode implementar seu próprio envelope de mensagem. Um usuário pode implementar seu próprio event_queue_hook. E assim por diante
Onde o SObjectizer pode ou não pode ser usado?
É muito mais fácil dizer onde o SObjectizer não pode ser usado por razões objetivas. Então, começamos a discussão enumerando essas áreas e, em seguida, daremos alguns exemplos do uso do SObjectizer no passado (e não apenas no passado).
Onde o SObjectizer não pode ser usado?
Como já foi dito acima, os modelos de ator e CSP não são uma boa opção para computação de alto desempenho e outras áreas da computação paralela. Portanto, se você precisar fazer várias matrizes ou transcodificar fluxos de vídeo, ferramentas como OpenMP, Intel TBB, cpp-taskflow, HPX ou MPI serão mais adequadas.
Sistemas rígidos em tempo real
Apesar do SObjectizer ter suas raízes nos sistemas SCADA, a implementação atual do SObjectizer (também conhecida como SObjectizer-5) não pode ser usada em sistemas rígidos em tempo real. Isso ocorre principalmente devido ao uso de memória dinâmica na implementação do SObjectizer: as mensagens são objetos alocados dinamicamente (no entanto, o SObjectizer pode usar objetos pré-alocados como mensagens), os despachantes usam a memória dinâmica nas filas de mensagens, e os prazos dos estados dos agentes usam objetos alocados dinamicamente. para verificar o tempo.
Infelizmente, o termo "tempo real" é muito usado no mundo moderno. Costuma-se dizer sobre serviços da web em tempo real, como "aplicativo da web em tempo real" ou "análise da web em tempo real" e assim por diante. O termo "on-line" ou "ao vivo" é mais apropriado para esses aplicativos do que o termo "em tempo real", mesmo na forma "soft-real-time". Portanto, se falamos de algo como "aplicativo da web em tempo real", o SObjectizer pode ser facilmente usado nesses sistemas "em tempo real".
Sistemas embarcados restritos
O SObjectizer conta com a biblioteca padrão C ++: std::thread
é usado para gerenciamento de threads, std::atomic
, std::mutex
, std::condition_variable
são usados para sincronização de dados, RTTI e dynamic_cast
são usados no SObjectizer de tamanho (por exemplo , std::type_index
são usados para identificação do tipo de mensagem), exceções C ++ são usadas para relatório de erros.
Isso significa que o SObjectizer não pode ser usado em ambientes onde esses recursos da biblioteca padrão não estão disponíveis. Por exemplo, no desenvolvimento de sistemas embarcados restritos, onde apenas uma parte do C ++ e C ++ stdlib pode ser usada.
Onde o SObjectizer foi usado no passado?
Agora tentamos falar brevemente sobre alguns casos de uso do SObjectizer no passado (e não apenas no passado). Infelizmente, não há informações completas porque existem alguns problemas.
Primeiro de tudo, não sabemos sobre todos os usos do SObjectizer. O SObjectizer é um software livre que pode ser usado mesmo em projetos proprietários. Então, algumas pessoas simplesmente obtêm o SObjectizer e o usam sem fornecer nenhum feedback para nós. Às vezes, adquirimos algumas informações sobre o uso do SObjectizer (mas sem detalhes), às vezes não sabemos nada.
O segundo problema é a permissão para compartilhar informações sobre o uso do SObjectizer em um projeto específico. Recebemos essa permissão muito raramente; na maioria dos casos, os usuários do SObjectizer não desejam abrir detalhes de implementação de seus projetos (às vezes entendemos os motivos, às vezes não).
Pedimos desculpas pelo fato de as informações fornecidas parecerem tão escassas e não conterem detalhes. No entanto, existem alguns exemplos de uso do SObjectizer:
- Gateway de agregação SMS / USSD que lida com mais de 500 milhões de mensagens por mês;
- parte do sistema que atende pagamentos on-line via caixas eletrônicos de um dos maiores bancos russos;
- modelagem de simulação de processos econômicos (como parte da pesquisa de doutorado);
- aquisição de dados distribuídos e sistema analítico. Dados coletados em pontos distribuídos mundialmente pelos comandos do nó central. O MQTT foi usado como transporte para controle e distribuição de dados adquiridos;
- ambiente de teste para verificação do sistema de controle em tempo real de equipamentos ferroviários;
- sistema de controle automático para cenário de teatro. Mais detalhes podem ser encontrados aqui ;
- componentes da plataforma de gerenciamento de dados em um sistema de publicidade on-line.
Uma amostra do SObjectizer
Vamos ver vários exemplos simples para provar o SObjectizer. Esses são exemplos muito simples que, esperamos, não exigem explicações adicionais, excluindo os comentários no código.
O exemplo tradicional "Olá, Mundo" no estilo do modelo de ator
O exemplo mais simples, com apenas um agente que reage à mensagem hello
e termina seu trabalho:
#include <so_5/all.hpp> // Message to be sent to an agent. struct hello { std::string greeting_; }; // Demo agent. class demo final : public so_5::agent_t { void on_hello(mhood_t<hello> cmd) { std::cout << "Greeting received: " << cmd->greeting_ << std::endl; // Now agent can finish its work. so_deregister_agent_coop_normally(); } public: // There is no need is a separate constructor. using so_5::agent_t::agent_t; // Preparation of agent to work inside SObjectizer. void so_define_agent() override { // Subscription to 'hello' message. so_subscribe_self().event(&demo::on_hello); } }; int main() { // Run SObjectizer instance. so_5::launch([](so_5::environment_t & env) { // Make and register an instance of demo agent. auto mbox = env.introduce_coop([](so_5::coop_t & coop) { auto * a = coop.make_agent<demo>(); return a->so_direct_mbox(); }); // Send hello message to registered agent. so_5::send<hello>(mbox, "Hello, World!"); }); }
Outra versão do "Hello, World" com agentes e modelo de publicação / assinatura
O exemplo mais simples com vários agentes, todos eles reagem à mesma instância da mensagem hello
:
#include <so_5/all.hpp> using namespace std::string_literals; // Message to be sent to an agent. struct hello { std::string greeting_; }; // Demo agent. class demo final : public so_5::agent_t { const std::string name_; void on_hello(mhood_t<hello> cmd) { std::cout << name_ << ": greeting received: " << cmd->greeting_ << std::endl; // Now agent can finish its work. so_deregister_agent_coop_normally(); } public: demo(context_t ctx, std::string name, so_5::mbox_t board) : agent_t{std::move(ctx)} , name_{std::move(name)} { // Create a subscription for hello message from board. so_subscribe(board).event(&demo::on_hello); } }; int main() { // Run SObjectizer instance. so_5::launch([](so_5::environment_t & env) { // Mbox to be used for speading hello message. auto board = env.create_mbox(); // Create several agents in separate coops. for(const auto & n : {"Alice"s, "Bob"s, "Mike"s}) env.register_agent_as_coop(env.make_agent<demo>(n, board)); // Spread hello message to all subscribers. so_5::send<hello>(board, "Hello, World!"); }); }
Se executarmos esse exemplo, podemos receber algo assim:
Alice: greeting received: Hello, World! Bob: greeting received: Hello, World! Mike: greeting received: Hello, World!
Exemplo "Olá, Mundo" no estilo CSP
Vejamos um exemplo de SObjectizer sem nenhum ator, apenas std::thread
e canais do tipo CSP.
Versão muito simples
Esta é uma versão muito simples que não é exceção segura:
#include <so_5/all.hpp> // Message to be sent to a channel. struct hello { std::string greeting_; }; void demo_thread_func(so_5::mchain_t ch) { // Wait while hello received. so_5::receive(so_5::from(ch).handle_n(1), [](so_5::mhood_t<hello> cmd) { std::cout << "Greeting received: " << cmd->greeting_ << std::endl; }); } int main() { // Run SObjectizer in a separate thread. so_5::wrapped_env_t sobj; // Channel to be used. auto ch = so_5::create_mchain(sobj); std::thread demo_thread{demo_thread_func, ch}; // Send a greeting. so_5::send<hello>(ch, "Hello, World!"); // Wait for demo thread. demo_thread.join(); }
Versão mais robusta, mas ainda simples
Esta é uma versão modificada do exemplo mostrado acima com a adição da exceção de segurança:
#include <so_5/all.hpp> // Message to be sent to a channel. struct hello { std::string greeting_; }; void demo_thread_func(so_5::mchain_t ch) { // Wait while hello received. so_5::receive(so_5::from(ch).handle_n(1), [](so_5::mhood_t<hello> cmd) { std::cout << "Greeting received: " << cmd->greeting_ << std::endl; }); } int main() { // Run SObjectizer in a separate thread. so_5::wrapped_env_t sobj; // Demo thread. We need object now, but thread will be started later. std::thread demo_thread; // Auto-joiner for the demo thread. auto demo_joiner = so_5::auto_join(demo_thread); // Channel to be used. This channel will be automatically closed // in the case of an exception. so_5::mchain_master_handle_t ch_handle{ so_5::create_mchain(sobj), so_5::mchain_props::close_mode_t::retain_content }; // Now we can run demo thread. demo_thread = std::thread{demo_thread_func, *ch_handle}; // Send a greeting. so_5::send<hello>(*ch_handle, "Hello, World!"); // There is no need to wait for something explicitly. }
Um exemplo bastante simples do HSM: blinking_led
Este é um exemplo padrão da distribuição do SObjectizer. O agente principal deste exemplo é um HSM que pode ser descrito pelo seguinte gráfico de estados:

O código fonte do exemplo:
#include <iostream> #include <so_5/all.hpp> class blinking_led final : public so_5::agent_t { state_t off{ this }, blinking{ this }, blink_on{ initial_substate_of{ blinking } }, blink_off{ substate_of{ blinking } }; public : struct turn_on_off final : public so_5::signal_t {}; blinking_led( context_t ctx ) : so_5::agent_t{ ctx } { this >>= off; off.just_switch_to< turn_on_off >( blinking ); blinking.just_switch_to< turn_on_off >( off ); blink_on .on_enter( []{ std::cout << "ON" << std::endl; } ) .on_exit( []{ std::cout << "off" << std::endl; } ) .time_limit( std::chrono::milliseconds{1500}, blink_off ); blink_off .time_limit( std::chrono::milliseconds{750}, blink_on ); } }; int main() { try { so_5::launch( []( so_5::environment_t & env ) { auto m = env.introduce_coop( []( so_5::coop_t & coop ) { auto led = coop.make_agent< blinking_led >(); return led->so_direct_mbox(); } ); auto pause = []( unsigned int v ) { std::this_thread::sleep_for( std::chrono::seconds{v} ); }; std::cout << "Turn blinking on for 10s" << std::endl; so_5::send< blinking_led::turn_on_off >( m ); pause( 10 ); std::cout << "Turn blinking off for 5s" << std::endl; so_5::send< blinking_led::turn_on_off >( m ); pause( 5 ); std::cout << "Turn blinking on for 5s" << std::endl; so_5::send< blinking_led::turn_on_off >( m ); pause( 5 ); std::cout << "Stopping..." << std::endl; env.stop(); } ); } catch( const std::exception & ex ) { std::cerr << "Error: " << ex.what() << std::endl; } return 0; }
Temporizadores, controle de sobrecarga para um agente e despachante active_obj
O controle de sobrecarga é um dos principais problemas para os atores: as filas de mensagens para os atores geralmente são ilimitadas e isso pode levar a um crescimento descontrolado de filas se um produtor de mensagens rápidas enviar mensagens mais rapidamente, em seguida, o destinatário poderá lidar com elas. O exemplo a seguir mostra os recursos do SObjectizer como limites de mensagens . Permite limitar a contagem de mensagens na fila do agente e defender o destinatário de mensagens redundantes.
Este exemplo também mostra o uso do timer na forma de uma mensagem periódica. A ligação de agentes ao dispatcher active_obj também é mostrada lá. A ligação a esse despachante significa que todo agente da cooperativa funcionará no próprio segmento de trabalho (por exemplo, um agente se torna um objeto ativo).
#include <so_5/all.hpp> using namespace std::chrono_literals; // Message to be sent to the consumer. struct task { int task_id_; }; // An agent for utilization of unhandled tasks. class trash_can final : public so_5::agent_t { public: // There is no need is a separate constructor. using so_5::agent_t::agent_t; // Preparation of agent to work inside SObjectizer. void so_define_agent() override { // Subscription to 'task' message. // Event-handler is specified in the form of a lambda-function. so_subscribe_self().event([](mhood_t<task> cmd) { std::cout << "unhandled task: " << cmd->task_id_ << std::endl; }); } }; // The consumer of 'task' messages. class consumer final : public so_5::agent_t { public: // We need the constructor. consumer(context_t ctx, so_5::mbox_t trash_mbox) : so_5::agent_t{ctx + // Only three 'task' messages can wait in the queue. limit_then_redirect<task>(3, // All other messages will go to that mbox. [trash_mbox]{ return trash_mbox; })} { // Define a reaction to incoming 'task' message. so_subscribe_self().event([](mhood_t<task> cmd) { std::cout << "handling task: " << cmd->task_id_ << std::endl; std::this_thread::sleep_for(75ms); }); } }; // The producer of 'test' messages. class producer final : public so_5::agent_t { const so_5::mbox_t dest_; so_5::timer_id_t task_timer_; int id_counter_{}; // Type of periodic signal to produce new 'test' message. struct generate_next final : public so_5::signal_t {}; void on_next(mhood_t<generate_next>) { // Produce a new 'task' message. so_5::send<task>(dest_, id_counter_); ++id_counter_; // Should the work be stopped? if(id_counter_ >= 10) so_deregister_agent_coop_normally(); } public: producer(context_t ctx, so_5::mbox_t dest) : so_5::agent_t{std::move(ctx)} , dest_{std::move(dest)} {} void so_define_agent() override { so_subscribe_self().event(&producer::on_next); } // This method will be automatically called by SObjectizer // when agent starts its work inside SObjectizer Environment. void so_evt_start() override { // Initiate a periodic message with no initial delay // and repetition every 25ms. task_timer_ = so_5::send_periodic<generate_next>(*this, 0ms, 25ms); } }; int main() { // Run SObjectizer instance. so_5::launch([](so_5::environment_t & env) { // Make and register coop with agents. // All agents will be bound to active_obj dispatcher and will // work on separate threads. env.introduce_coop( so_5::disp::active_obj::make_dispatcher(env).binder(), [](so_5::coop_t & coop) { auto * trash = coop.make_agent<trash_can>(); auto * handler = coop.make_agent<consumer>(trash->so_direct_mbox()); coop.make_agent<producer>(handler->so_direct_mbox()); }); }); }
Se executarmos esse exemplo, podemos ver a seguinte saída:
handling task: 0 handling task: 1 unhandled task: 5 unhandled task: 6 handling task: 2 unhandled task: 8 unhandled task: 9 handling task: 3 handling task: 4 handling task: 7
Esta saída mostra que várias mensagens que não cabem no limite definido são rejeitadas e redirecionadas para outro receptor.
Mais exemplos
Um exemplo mais ou menos semelhante ao código de aplicativos da vida real pode ser encontrado em nosso projeto de demonstração do Shrimp . Outro conjunto de exemplos interessantes pode ser encontrado nesta minissérie sobre o clássico "problema dos filósofos gastronômicos": parte 1 e parte 2 . E, é claro, existem muitos exemplos no próprio SObjectizer .
Há uma resposta muito simples: é mais do que suficiente para nós. O SObjectizer pode distribuir milhões de mensagens por segundo e a velocidade real depende dos tipos de expedidores usados, tipos de mensagens, perfil de carga, hardware / OS / compilador usado e assim por diante. Em uma aplicação real, geralmente usamos apenas uma fração da velocidade do SObjectizer.
O desempenho do SObjectizer para sua tarefa específica depende muito da sua tarefa, da solução específica dessa tarefa, do seu hardware ou ambiente virtual, da versão do seu compilador e do seu sistema operacional. Portanto, a melhor maneira de encontrar uma resposta para essa pergunta é criar um benchmark específico para sua tarefa e fazer experiências com ela.
Se você deseja números de alguns benchmarks sintéticos, existem alguns programas na pasta test / so_5 / bench da distribuição do SObjectizer.
Pensamos que um jogo de benchmarking comparando a velocidade de diferentes ferramentas é um jogo de marketing. Fizemos uma tentativa no passado, mas rapidamente percebemos que é apenas uma perda de tempo. Portanto, não jogamos esse jogo agora. Nós gastamos nosso tempo e nossos recursos apenas em benchmarks que permitem verificar a ausência de degradação do desempenho, resolver alguns casos extremos (como o desempenho de mboxes MPMC com grande quantidade de assinantes ou o desempenho de um agente com centenas de milhares de assinaturas), para acelerar algumas operações específicas do SObjectizer (como registro / cancelamento de registro de uma cooperativa).
Então deixamos a comparação de velocidade para quem gosta desse jogo e tem tempo para jogá-lo.
Por que o SObjectizer parece exatamente como é?
Existem várias "estruturas de ator" para C ++, e todas elas parecem diferentes. Parece que tem algumas razões objetivas: toda estrutura possui características únicas e tem objetivos diferentes. Além disso, os atores em C ++ podem ser implementados de maneira muito diferente. Portanto, a questão principal não é "por que o framework X não se parece com o framework Y?", Mas "por que o framework X parece com o que é?"
Agora, tentaremos descrever brevemente alguns motivos por trás dos principais recursos do SObjectizer. Esperamos que isso permita uma melhor compreensão das habilidades do SObjectizer. Mas antes de começarmos, é necessário mencionar uma coisa muito importante: o SObjectizer nunca foi um experimento. Foi criado para resolver o trabalho da vida real e tem evoluído com base na experiência da vida real.
Agentes são objetos de classes derivadas de agent_t
Agentes (também conhecidos como atores) no SObjectzer são objetos de classes definidas pelo usuário que devem ser derivadas de uma classe especial agent_t
. Pode parecer redundante em pequenos exemplos de brinquedos, mas nossa experiência mostra que essa abordagem simplifica muito o desenvolvimento de software real, em que os agentes geralmente têm o tamanho em várias centenas de linhas (você pode ver um dos exemplos aqui , mas esta postagem no blog está em Russo). Às vezes até em vários milhares de linhas.
A experiência mostra que um agente simples com a primeira versão em cem linhas se torna muito mais gordo e complexo nos próximos anos de evolução. Então, depois de cinco anos, você poderá encontrar um monstro em mil linhas com dezenas de métodos.
O uso de classes nos permite gerenciar a complexidade dos agentes. Nós podemos usar herança de classes. E também podemos usar classes de modelo. Essas são técnicas muito úteis que simplificam bastante o desenvolvimento de famílias de agentes com lógica semelhante por dentro.
Mensagens como objetos de estruturas / classes de usuários
Mensagens no SObjectizer são objetos de estruturas ou classes definidas pelo usuário. Há pelo menos duas razões para isso:
- o desenvolvimento do SObjectizer-5 começou em 2010 quando o C ++ 11 ainda não havia sido padronizado. Portanto, no início, não podíamos usar recursos do C ++ 11 como modelos variados e classe
std::tuple
. A única opção que tivemos foi o uso de um objeto de uma classe herdada de uma classe especial message_t
. Agora não há necessidade de derivar o tipo de mensagem de message_t
, mas o SObjectizer agrupa um objeto de usuário em um objeto derivado de message_t
qualquer maneira, sob o capô; - o conteúdo de uma mensagem pode ser facilmente alterado sem modificação das assinaturas dos manipuladores de eventos. E existe um controle de um compilador: se você remover algum campo de uma mensagem ou alterar seu tipo, o compilador informará sobre o acesso incorreto a esse campo.
O uso de mensagens como objetos também permite trabalhar com mensagens pré-alocadas e armazenar uma mensagem recebida em algum contêiner e reenviá-la mais tarde.
Cooperativas de agentes
Uma cooperativa de agentes é provavelmente um dos recursos exclusivos do SObjectizer. Uma cooperativa é um grupo de agentes que devem ser adicionados e removidos do SObjectizer de maneira transacional. Isso significa que se uma cooperativa contiver três agentes, todos esses agentes deverão ser adicionados ao SObjectizer com êxito ou nenhum deles deverá ser adicionado. Da mesma forma, todos os três agentes devem ser removidos do SObjectizer ou os três agentes devem continuar seu trabalho.
A necessidade de cooperativas foi descoberta logo após o início da vida útil do SObjectizer. Tornou-se óbvio que os agentes seriam criados por grupos, não por instâncias únicas. As cooperativas foram inventadas para simplificar a vida de um desenvolvedor: não há necessidade de controlar a criação do próximo agente e remover agentes criados anteriormente se a criação de um novo agente falhar.
Uma cooperativa também pode ser vista como um supervisor no modo tudo por um: se um agente da cooperativa falhar, a cooperativa inteira será removida do SObjectizer Environment e destruída (um usuário pode reagir a isso e recriar a cooperativa novamente).
Caixas de mensagem
As caixas de mensagens são outro recurso exclusivo do SObjectizer. As mensagens no SObjectizer são enviadas para uma caixa de mensagens (mbox), não para um agente diretamente. Pode haver um receptor atrás da mbox, ou pode haver um milhão de assinantes ou não pode haver ninguém.
Mboxes nos permite suportar a funcionalidade básica do modelo Publicar-Assinar. Uma mbox pode ser vista como MQ-broker e o tipo de mensagem pode ser visto como um tópico.
Mboxes nos permite também implementar várias formas interessantes de entrega de mensagens. Por exemplo, há uma mbox round-robin que espalha mensagens entre assinantes de maneira round-robin. Há também uma mbox retida que retém a última mensagem enviada e a reenvia automaticamente para cada novo assinante. Há também um wrapper simples em torno do libmosquitto que permite usar o MQTT como um transporte para um aplicativo distribuído.
Agentes como HSM
Os agentes no SObjectizer são máquinas de estado. Foi desde o início, simplesmente porque o SObjectizer tem raízes no campo SCADA, onde as máquinas de estado são usadas ativamente. Mas rapidamente ficou óbvio que os agentes na forma de uma máquina de estado podem ser úteis mesmo em diferentes nichos (como aplicativos de telecomunicações e finanças).
O suporte de máquinas de estado hierárquico (por exemplo, manipuladores on_enter / on_exit, estados aninhados, prazos e assim por diante) foi adicionado após algum tempo de uso do SObjectizer na produção. E esse recurso tornou o SObjectizer ainda mais poderoso e conveniente.
Uso de exceções em C ++
As exceções de C ++ são usadas no SObjectizer como o principal mecanismo de relatório de erros. Apesar do fato de que o uso da exceção C ++ às vezes pode ser caro, decidimos usar exceções em vez de códigos de erro.
Tivemos uma experiência negativa com códigos de erro no SObjectizer-4, onde as exceções não foram usadas. Isso levou à ignorância de erros no código do aplicativo e, algumas vezes, ações importantes não foram executadas porque ocorreu um erro ao criar um novo cooperativo ou ao enviar uma mensagem. Mas esse erro foi ignorado e esse fato foi descoberto muito mais tarde.
O uso de exceções de C ++ no SObjectizer-5 permite escrever código mais correto e robusto. Em casos comuns, as exceções são lançadas pelo SObjectizer muito raramente, portanto, o uso da exceção não tem impacto negativo no desempenho do SObjectizer ou no desempenho de aplicativos escritos na parte superior do SObjectizer.
Não há suporte para aplicativos distribuídos "prontos para uso"
O SObjectzer-5 não possui suporte interno para aplicativos distribuídos. Isso significa que o SObjectizer distribui mensagens apenas dentro de um processo. Se você precisar organizar a distribuição de mensagens entre processos ou entre notas, precisará integrar algum tipo de IPC em seu aplicativo.
Isso não ocorre porque não podemos implementar alguma forma de IPC no SObjectizer. Já tínhamos isso no SObjectizer-4. E como temos essa experiência, decidimos não fazer isso no SObjectizer-5. Aprendemos que não existe um tipo de IPC que se encaixe perfeitamente para diferentes condições.
Se você deseja ter uma boa comunicação entre nós em seu aplicativo, é necessário selecionar os protocolos subjacentes apropriados. Por exemplo, se você precisar espalhar milhões de pacotes pequenos com alguns dados de vida curta (como a distribuição de medidas das condições climáticas atuais), precisará usar um IPC. Mas se você precisar transferir BLOBs enormes (como fluxos de vídeo 4K / 8K ou arquivos com dados financeiros internos), precisará usar outro tipo de IPC.
E não falamos de introperabilidade com software escrito em diferentes idiomas ...
Você pode acreditar que alguma "estrutura de ator" universal pode fornecer um IPC que será bom para diferentes condições. Mas sabemos que é apenas besteira de marketing. Nossa experiência nos mostra que é muito mais simples e seguro adicionar o IPC necessário em seu aplicativo e depois confiar em idéias, necessidades e conhecimentos de autores de uma "estrutura de ator" de terceiros.
O SObjectizer permite incorporar vários tipos de IPC na forma de mboxes personalizadas. Portanto, permite ocultar o fato da distribuição de mensagens em uma rede dos usuários de um SObjectizer.
Em vez da conclusão
A estrutura do SObjectizer não é grande, mas não é pequena. Portanto, é impossível dar ao leitor uma impressão bastante profunda sobre o SObjectizer em apenas uma visão geral. Por isso, convidamos você a dar uma olhada no projeto SObjectizer.
O SObjectizer em si vive no GitHub . Existe o Wiki do projeto no GitHub e recomendamos iniciar no SObjectizer 5.6 Noções básicas e depois ir para artigos da série In-depth. Para aqueles que querem ir mais fundo, podemos recomendar Vamos dar uma olhada na seção de capas do SObjectizer .
Se você tiver alguma dúvida, pode nos fazer uma pergunta no grupo SObjectizer nos grupos do Google.