SObjectizer-5.6.0: corte ao vivo para crescer ainda mais


No terceiro dia, uma nova versão do SObjectizer ficou disponível : 5.6.0 . Sua principal característica é a rejeição da compatibilidade com o ramo estável anterior 5.5, que vem se desenvolvendo constantemente ao longo de quatro anos e meio.


Os princípios básicos de operação do SObjectizer-5 permaneceram os mesmos. Comunicações, agentes, cooperações e despachantes ainda estão conosco. Mas algo mudou seriamente, algo geralmente foi jogado fora. Portanto, apenas pegar o SO-5.6.0 e recompilar seu código falhará. Algo precisa ser reescrito. Algo pode ter que ser redesenhado.


Por que cuidamos da compatibilidade por vários anos e depois decidimos levar e quebrar tudo? E o que quebrou mais completamente?


Vou tentar falar sobre isso neste artigo.


Por que você precisou quebrar alguma coisa?


É simples assim.


O SObjectizer-5.5 durante seu desenvolvimento absorveu tantas coisas diferentes e diversas que não foram originalmente planejadas que, como resultado, formou muitas muletas e adereços dentro. A cada nova versão, adicionar algo novo ao SO-5.5 ficava cada vez mais difícil. E, finalmente, à pergunta "Por que precisamos de tudo isso?" Nenhuma resposta adequada foi encontrada.


Portanto, a primeira razão é a re-complicação das crianças do SObjectizer.


A segunda razão é que estamos estupidamente cansados ​​de focar nos antigos compiladores C ++. A filial 5.5 começou em 2014, quando tínhamos, se não me engano, gcc-4.8 e MSVS2013. E nesse nível, continuamos a manter a barra de requisitos para o nível de suporte do padrão C ++.


Inicialmente, tínhamos "interesse egoísta" nisso. Além disso, por algum tempo, consideramos os baixos requisitos para a qualidade do suporte ao padrão C ++ como nossa "vantagem competitiva".


Mas o tempo passa, o "interesse egoísta" acabou. Alguns benefícios dessa "vantagem competitiva" não são visíveis. Talvez eles fossem, se trabalhassemos com o C ++ 98, estaríamos interessados ​​em uma empresa sangrenta. Mas o sangrento empreendimento daqueles como nós, em princípio, não está interessado. Portanto, foi decidido parar de nos limitar e tomar algo mais fresco. Então, pegamos o mais novo do estábulo no momento: C ++ 17.


Obviamente, nem todo mundo vai gostar dessa solução, afinal, para muitos C ++ 17, essa é agora uma vantagem inatingível, que ainda está muito, muito distante.


No entanto, decidimos esse risco. Mesmo assim, o processo de popularização do SObjectizer não está indo rápido; portanto, quando o SObjectizer se tornar mais ou menos amplamente procurado, o C ++ 17 não será mais uma "vanguarda". Em vez disso, ele será tratado da mesma maneira que é agora no C ++ 11.


Em geral, em vez de continuar construindo muletas usando um subconjunto do C ++ 11, decidimos refazer seriamente as partes internas do SObjectizer usando o C ++ 17. Construir uma base sobre a qual o SObjectizer possa se desenvolver progressivamente nos próximos quatro ou cinco anos.


O que mudou seriamente no SObjectizer-5.6?


Agora, vamos examinar brevemente algumas das mudanças mais marcantes.


As cooperações do agente não têm mais nomes de sequência


O problema


Desde o início, o SObjectizer-5 exigiu que cada cooperação tivesse seu próprio nome de string exclusivo. Esse recurso foi herdado do quinto SObjectizer do quarto SObjectizer anterior.


Consequentemente, o SObjectizer precisava armazenar os nomes das cooperações registradas. Verifique sua singularidade no registro. Procure cooperação por nome durante o cancelamento de registro, etc., etc.


Desde as primeiras versões, um esquema simples foi usado no SObjectzer-5: um único dicionário de cooperações registradas protegidas por mutex. Ao registrar uma cooperação, o mutex é capturado, a exclusividade do nome da cooperação, a presença de um pai, etc. Após a verificação, o dicionário é modificado, após o qual o mutex é liberado. Isso significa que, ao mesmo tempo em que o registro / cancelamento de registro de várias cooperações começa ao mesmo tempo, em alguns momentos eles param e esperam até que uma das operações conclua o trabalho com o dicionário cooperativo. Por esse motivo, as operações cooperativas não foram bem dimensionadas.


Era disso que eu queria me livrar, a fim de melhorar a situação com a velocidade do registro das cooperações.


Solução


Duas maneiras principais de resolver esse problema foram consideradas.


Primeiramente, armazenando nomes de strings, mas alterando a maneira como o dicionário é armazenado para que a operação de registro de cooperação possa ser dimensionada. Por exemplo, compartilhamento de dicionário, ou seja, quebrando-o em vários pedaços, cada um dos quais seria protegido por seu mutex.


Em segundo lugar, uma rejeição completa dos nomes das strings e o uso de alguns identificadores atribuídos pelo SObjectizer.


Como resultado, escolhemos o segundo método e abandonamos completamente o nome das cooperativas. Agora, no SObjectizer, existe algo como coop_handle , ou seja, um identificador cujo conteúdo está oculto do usuário, mas que pode ser comparado com std::weak_ptr .


O SObjectizer retorna coop_handle ao registrar uma colaboração:


 auto coop = env.make_coop(); ... //    . auto coop_id = env.register_coop(std::move(coop)); // . //   coop_id    . 

Esse identificador deve ser usado para cancelar o registro da cooperação:


 auto coop = env.make_coop(); ... //    . auto coop_id = env.register_coop(std::move(coop)); // . //   coop_id    . ... // - . // ,     . //       . env.deregister_coop(coop_id, ...); 

Além disso, esse identificador deve ser usado ao estabelecer um relacionamento pai-filho:


 //   . auto parent = env.make_coop(); ... //  parent . auto parent_id = env.register_coop(std::move(parent)); //  . ... //      ,    . auto child = env.make_coop(parent_id); ... 

A estrutura do repositório para cooperação no SObjectizer Environment também mudou drasticamente. Se antes da versão 5.5 inclusive era um dicionário comum, agora cada cooperação é um repositório de links para cooperações filho. I.e. As cooperativas formam uma árvore com uma raiz em uma cooperativa de raiz especial oculta ao usuário.


Essa estrutura torna possível dimensionar as deregister_coop register_coop e deregister_coop muito melhor: o bloqueio mútuo de operações paralelas ocorre apenas se ambas pertencerem à mesma cooperação dos pais. Para maior clareza, eis o resultado do lançamento de uma referência especial que mede o desempenho das operações com cooperações no meu laptop antigo com o Ubuntu 16.04 e GCC-7.3:


 _test.bench.so_5.parallel_parent_child -r 4 -l 7 -s 5 Configuration: roots: 4, levels: 7, level-size: 5 parallel_parent_child: 15.69s 488280 488280 488280 488280 Total: 1953120 

I.e. a versão 5.6.0 lidou com quase 2 milhões de cooperações em ~ 15,5 segundos.


E aqui está a versão 5.5.24.4, a última da ramificação 5.5 no momento:


 _test.bench.so_5.parallel_parent_child -r 4 -l 7 -s 5 Configuration: roots: 4, levels: 7, level-size: 5 parallel_parent_child: 46.856s 488280 488280 488280 488280 Total: 1953120 

O mesmo cenário, mas o resultado é três vezes pior.


Resta apenas um tipo de despachante


Os expedidores são uma das pedras angulares do SObjectizer. São os despachantes que determinam onde e como os agentes processarão suas mensagens. Portanto, sem a idéia de despachantes, provavelmente não haveria um SObjectizer.


No entanto, os próprios despachantes evoluíram, evoluíram e evoluíram a um ponto em que não era difícil para nós criar um novo despachante para o SObjectizer-5.5. Mas muito problemático. No entanto, vamos levá-lo em ordem.


Inicialmente, todos os despachantes necessários ao aplicativo só podiam ser criados no início do SObjectizer:


 so_5::launch( []( so_5::environment_t & env ) { /* -   */ }, //    SObjectizer-. []( so_5::environment_params_t & params ) { p.add_named_dispatcher("active_obj", so_5::disp::active_obj::create_disp()); p.add_named_dispatcher("shutdowner", so_5::disp::active_obj::create_disp()); p.add_named_dispatcher("groups", so_5::disp::active_group::create_disp()); ... } ); 

Eu não criei o expedidor necessário antes do início - tudo, é minha culpa, você não pode mudar nada.


Está claro que isso é inconveniente e, à medida que os cenários de uso do SObjectizer se expandiam, era necessário resolver esse problema. Portanto, o método add_dispatcher_if_not_exists , que verificava a presença de um expedidor e, se não houvesse, permitia criar uma nova instância:


 so_5::launch( []( so_5::environment_t & env ) { ... // - . //     . env.add_dispatcher_if_not_exists( "extra_dispatcher", []{ return so_5::disp::active_obj::create_disp(); } ); }, //    SObjectizer-. []( so_5::environment_params_t & params ) {...} ); 

Esses despachantes foram chamados de públicos. Os expedidores públicos tinham nomes únicos. E usando esses nomes, os agentes estavam vinculados aos despachantes:


 so_5::launch( []( so_5::environment_t & env ) { ... // - . //     . env.add_dispatcher_if_not_exists( "extra_dispatcher", []{ return so_5::disp::active_obj::create_disp(); } ); //         //    . auto coop = env.create_coop( "ping_pong", //     extra_dispatcher. so_5::disp::active_obj::create_disp_binder( "extra_dispatcher" ) ); coop->make_agent< a_pinger_t >(...); coop->make_agent< a_ponger_t >(...); ... }, //    SObjectizer-. []( so_5::environment_params_t & params ) {...} ); 

Mas os despachantes públicos tinham uma característica desagradável. Eles começaram a trabalhar imediatamente após serem adicionados ao SObjectizer Environment e continuaram a trabalhar até o SObjectizer Environment concluir seu trabalho.


Mais uma vez, com o tempo, começou a interferir. Era necessário garantir que os despachantes pudessem ser adicionados conforme necessário e que os despachantes que se tornassem desnecessários fossem excluídos automaticamente.


Portanto, havia despachantes "particulares". Esses despachantes não tinham nomes e viveram enquanto houvesse referências a eles. Despachantes particulares podem ser criados a qualquer momento após iniciar o SObjectizer Environment, eles são destruídos automaticamente.


Em geral, os despachantes privados acabaram sendo um elo de muito sucesso na evolução dos despachantes, mas trabalhar com eles foi muito diferente de trabalhar com despachantes públicos:


 so_5::launch( []( so_5::environment_t & env ) { ... // - . //     . auto disp = so_5::disp::active_obj::create_private_disp(env); //         //    . auto coop = env.create_coop( "ping_pong", //      . disp->binder() ); coop->make_agent< a_pinger_t >(...); coop->make_agent< a_ponger_t >(...); ... }, //    SObjectizer-. []( so_5::environment_params_t & params ) {...} ); 

Ainda mais despachantes públicos e privados diferiram na implementação. Portanto, para não duplicar o código e escrever separadamente o despachante público e privado do mesmo tipo, tive que usar construções bastante complexas com modelos e herança.


Como resultado, eu estava cansado de acompanhar toda essa variedade e no SObjectizer-5.6 havia apenas um tipo de despachante. De fato, este é um análogo de despachantes particulares. Mas apenas sem menção explícita à palavra "particular". Então agora o fragmento mostrado acima será escrito como:


 so_5::launch( []( so_5::environment_t & env ) { ... // - . //     . auto disp = so_5::disp::active_obj::make_dispatcher(env); //         //    . auto coop = env.create_coop( "ping_pong", //      . disp.binder() ); coop->make_agent< a_pinger_t >(...); coop->make_agent< a_ponger_t >(...); ... }, //    SObjectizer-. []( so_5::environment_params_t & params ) {...} ); 

Existem apenas funções gratuitas send, send_delayed e send_periodic left


O desenvolvimento da API para enviar mensagens para o SObjectizer é provavelmente o exemplo mais impressionante de como o SObjectizer mudou à medida que o suporte ao C ++ 11 melhorou nos compiladores disponíveis para nós.


Primeiro, as mensagens foram enviadas assim:


 mbox->deliver_message(new my_message(...)); 

Ou, se você seguir as "recomendações dos melhores criadores de cães" (c):


 std::unique_ptr<my_message> msg(new my_message(...)); mbox->deliver_message(std::move(msg)); 

No entanto, chegamos à nossa disposição os compiladores com suporte para modelos variados e funções de envio. Tornou-se possível escrever assim:


 send<my_message>(target, ...); 

É verdade que demorou mais tempo para uma família inteira send_to_agent com um simples send , incluindo send_to_agent , send_delayed_to_agent etc. E então, para tornar essa família mais restrita ao conjunto familiar de send , send_delayed e send_periodic .


Mas, apesar de a família de funções de envio ter sido formada há muito tempo e ter sido a maneira recomendada de enviar mensagens por vários anos, métodos antigos como deliver_message , schedule_timer e single_timer ainda estavam disponíveis para o usuário.


Mas na versão 5.6.0, apenas as funções de send gratuito, send_delayed e send_periodic foram salvas na API pública do SObjectizer. Todo o resto foi excluído por completo ou transferido para espaços de nomes internos do SObjectizer.


Portanto, no SObjectizer-5.6, a interface para o envio de mensagens finalmente se tornou o que teria sido se tivéssemos compiladores com suporte normal ao C ++ 11 desde o início. Bem, além disso, se tivéssemos experiência em usar esse C ++ 11 muito normal.


Formato único send_delayed e send_periodic


Com as send_periodic e send_periodic nas versões anteriores do SObjectizer, houve outro incidente.


Para usar o timer, você deve ter acesso ao SObjectizer Environment. Dentro do agente, há um link para o SObjectizer Environment. E dentro do mchain existe esse link. Mas dentro da mbox ela não estava lá. Portanto, se uma mensagem pendente foi enviada para um agente ou para o mchain, a chamada send_delayed com:


 send_delayed<my_message>(target_agent, pause, ...); send_delayed<my_message>(target_mchain, pause, ...); 

Para o caso da mbox, tivemos que pegar um link para o SObjectizer Environment de outro lugar:


 send_delayed<my_message>(this->so_environment(), target_mbox, pause, ...); 

Esse recurso de send_delayed e send_periodic foi uma pequena lasca. O que não interfere muito, mas é irritante. E tudo porque, inicialmente, não começamos a armazenar o link para o SObjectizer Environment no mbox-ahs.


A violação da compatibilidade com versões anteriores foi um bom motivo para se livrar dessa lasca.


Agora você pode descobrir na mbox para que ambiente do SObjectizer foi criado. E isso possibilitou o uso dos send_periodic e send_periodic para qualquer tipo de destinatário da mensagem de timer:


 send_delayed<my_message>(target_agent, pause, ...); send_delayed<my_message>(target_mchain, pause, ...); send_delayed<my_message>(target_mbox, pause, ...); 

No sentido literal, "um pouco, mas legal".


Não há mais agentes ad-hoc


Como diz o ditado, "Todo acidente tem um nome, nome do meio e sobrenome". No caso de agentes ad-hoc, esse é meu primeiro nome, nome do meio e sobrenome :(


O ponto é este. Quando começamos a falar sobre o SObjectizer-5 em público, ouvimos muitas críticas pela veracidade do código nos exemplos do SObjectizer. E, pessoalmente, essa verbosidade me pareceu um problema sério com o qual preciso lidar seriamente.


Uma fonte de verbosidade é a necessidade de os agentes herdarem do tipo base especial agent_t . E a partir disso, ao que parece, não há escapatória. Ou não?


Portanto, havia agentes ad-hoc, ou seja, agentes, para os quais não era necessário escrever uma classe separada, bastava definir a reação às mensagens na forma de funções lambda. Por exemplo, o exemplo clássico de pingue-pongue em agentes ad-hoc pode ser escrito assim:


 auto pinger = coop->define_agent(); auto ponger = coop->define_agent(); pinger .on_start( [ponger]{ so_5::send< msg_ping >( ponger ); } ) .event< msg_pong >( pinger, [ponger]{ so_5::send< msg_ping >( ponger ); } ); ponger .event< msg_ping >( ponger, [pinger]{ so_5::send< msg_pong >( pinger ); } ); 

I.e. sem classes próprias. Apenas chamamos define_agent() na cooperação e obtemos algum tipo de objeto agente, que você pode assinar nas mensagens recebidas.


Portanto, no SObjectizer-5 houve uma separação entre agentes regulares e ad-hoc.


O que não trouxe bônus visíveis, apenas os custos trabalhistas extras para acompanhar essa separação. E, com o tempo, ficou claro que os agentes ad-hoc são como uma mala sem alça: é difícil de carregar e é uma pena sair. Mas enquanto trabalhava no SObjectizer-5.6, foi decidido sair.


Ao mesmo tempo, também foi aprendida outra lição, talvez ainda mais importante: em qualquer discussão pública sobre a ferramenta na Internet, um grande número de pessoas participará, indiferentes ao que é a ferramenta, por que é necessária, por que é como deveria ser usada etc. É simplesmente importante que eles expressem sua opinião forte. No segmento da Internet em língua russa, além disso, ainda é muito importante transmitir aos desenvolvedores da ferramenta como eles são estúpidos e sem instrução e quanto o resultado de seu trabalho não é necessário.


Portanto, você deve ter muito cuidado com o que lhe disseram. E você pode ouvir (e depois com cuidado) apenas o que é dito aqui neste sentido: "Tentei fazer isso no seu instrumento e não gosto de quanto código ele chegou aqui". Mesmo esses desejos devem ser tratados com muito cuidado: "Eu aceitaria seu desenvolvimento se fosse mais fácil aqui e aqui".


Infelizmente, a habilidade de "filtragem" dita por "simpatizantes" na Internet há cerca de cinco anos era muito menor do que agora. Portanto, um experimento específico como agentes ad-hoc no SObjectizer.


SObjectizer-5.6 não oferece mais suporte à interação sincronizada de agentes


O tópico da interação sincronizada entre agentes é muito antigo e dolorido.


Tudo começou nos dias do SObjectizer-4. E no SObjectizer-5 continuou. Até agora, finalmente, o chamado solicitações de serviço . Que inicialmente, reconhecidamente, era assustador como a morte. Mas então eu consegui dar a eles uma aparência mais ou menos decente .


Mas esse acabou sendo o caso quando a primeira panqueca saiu irregular :(


No SObjectizer, eu tive que implementar a entrega e o processamento de mensagens regulares de uma maneira, e a entrega e o processamento de solicitações síncronas de outra. É especialmente triste que esses recursos tenham que ser levados em consideração, inclusive ao implementar suas próprias mbox-s.


E depois que a funcionalidade das mensagens de envelope foi adicionada ao SObjectizer, tornou-se necessário examinar com mais frequência e profundidade as diferenças entre mensagens regulares e solicitações síncronas.


Em geral, com solicitações síncronas durante a manutenção / desenvolvimento do SObjectizer, havia muita dor de cabeça. Tanto que, a princípio, havia um desejo concreto de se livrar dessas solicitações muito síncronas . E então esse desejo foi realizado.


E assim, no SObjectizer-5.6, os agentes podem interagir novamente apenas através de mensagens assíncronas.


E, como às vezes ainda é necessário algo como interação síncrona, o suporte para esse tipo de interação foi enviado ao projeto so5extra que o acompanha :


 //    "-". using my_request_reply = so_5::extra::sync::request_reply_t<my_request, my_reply>; ... //  ,    . class request_handler final : public so_5::agent_t { ... //  .      . void on_request(typename my_request_reply::request_mhood_t cmd) { ... //  . //      cmd->request(). //   . cmd->make_reply(...); //      my_reply. } ... void so_define_agent() override { //       . so_subscribe_self().event(&request_handler::on_request); } }; ... //     . so_5::mbox_t handler_mbox = ...; //        15s. //    ,    . my_reply reply = my_request_reply::ask_value(handler_mbox, 15s, ...); //       my_request. 

I.e. Agora, trabalhar com solicitações síncronas é fundamentalmente diferente, pois o manipulador de solicitações não retorna um valor do método manipulador, como era antes. Em vez disso, o método make_reply é make_reply .


A nova implementação é boa, pois a solicitação e a resposta são enviadas dentro do SObjectizer como mensagens assíncronas regulares. De fato, make_reply é uma implementação de send um pouco mais específica.


E, o mais importante, a nova implementação nos permitiu obter funcionalidades anteriormente inatingíveis:


  • solicitações síncronas (ou seja request_reply_t<Request, Reply> objetos request_reply_t<Request, Reply> ) agora podem ser salvas e / ou encaminhadas para outros manipuladores. O que torna possível implementar vários esquemas de balanceamento de carga;
  • Você pode fazer com que a resposta à solicitação seja enviada em uma mbox comum do agente que está iniciando a solicitação. E o agente iniciador processará a resposta da maneira usual, como qualquer outra mensagem;
  • Você pode enviar várias solicitações para diferentes destinatários ao mesmo tempo e analisar as respostas na ordem em que foram recebidas:

 using first_dialog = so_5::extra::sync::request_reply_t<first_request, first_reply>; using second_dialog = so_5::extra::sync::request_reply_t<second_request, second_reply>; //         . auto reply_ch = create_mchain(env); //     . first_dialog::initiate_with_custom_reply_to( one_service, reply_ch, so_5::extra::sync::do_not_close_reply_chain, ...); second_dialog::initiate_with_custom_reply_to( another_service, reply_ch, so_5::extra::sync::do_not_close_reply_chain, ...); //    . receive(from(reply_ch).handle_n(2).empty_timeout(15s), [](typename first_dialog::reply_mhood_t cmd) {...}, [](typename second_dialog::reply_mhood_t cmd) {...}); 

Portanto, podemos dizer que, com a interação síncrona no SObjectizer, aconteceu o seguinte:


  • por um longo tempo ele se foi por razões ideológicas;
  • então foi adicionado e descobriu-se que às vezes essa interação é útil;
  • mas a experiência mostrou que a primeira implementação não é muito bem-sucedida;
  • a antiga implementação foi completamente descartada e uma nova implementação foi proposta em troca.

Eles trabalharam com seus próprios erros, em geral.


Conclusão


Este artigo, brevemente, falou sobre várias alterações no SObjectizer-5.6.0 e os motivos por trás dessas alterações.


Uma lista mais completa de alterações pode ser encontrada aqui .


Concluindo, gostaria de oferecer àqueles que ainda não experimentaram o SObjectizer, aceitem e experimentem. E compartilhe conosco seus sentimentos: o que você gostou, o que você não gostou, o que estava faltando.


Ouvimos atentamente todos os comentários / sugestões construtivas. Além disso, nos últimos anos, apenas o que alguém precisa está incluído no SObjectizer. Portanto, se você não nos disser o que gostaria de ter no SObjectizer, isso não aparecerá. E se você me disser, quem sabe ...;)


O projeto agora vive e se desenvolve aqui . E para aqueles que estão acostumados a usar apenas o GitHub, há um espelho do GitHub . Esse espelho é completamente novo, para que você possa ignorar a falta de estrelas.


PS. Você pode acompanhar as notícias relacionadas ao SObjectizer neste grupo do Google . Lá, você pode levantar questões relacionadas ao SObjectizer.

Source: https://habr.com/ru/post/pt453256/


All Articles