O que há de novo no SObjectizer-5.7.0 e o que está aguardando este projeto a seguir?

O SObjectizer é uma estrutura C ++ 17 relativamente pequena que permite o uso de abordagens como Modelo do Ator, Publish-Subscribe e Communicating Sequential Processes (CSP) em programas C ++. O que simplifica bastante o desenvolvimento de aplicativos multiencadeados complexos em C ++. Se o leitor ouvir sobre o SObjectizer pela primeira vez, você poderá impressioná-lo nesta apresentação ou neste artigo já bastante antigo.


De um modo geral, não existem tantas ferramentas similares abertas, ainda vivas e ainda em desenvolvimento para C ++. Só podemos lembrar QP / C ++ , CAF: C ++ Actor Framework , actor-zeta e o projeto de rotor muito jovem. Há uma escolha, mas não tão grande.


Recentemente, outra versão "importante" do SObjectizer tornou-se disponível, onde finalmente apareceu uma coisa sobre a qual falamos há muito tempo, e cuja implementação eu várias vezes me aproximei sem sucesso. Podemos dizer que um marco foi alcançado. Essa também é uma ocasião para falar sobre o que o SObjectizer irá esperar após o lançamento da versão 5.7.0.


Suporte Send_case em select ()


Portanto, a inovação mais importante que apareceu na v.5.7.0 e para a qual a compatibilidade com a v.5.6 lançada no ano passado (e não quebramos a compatibilidade) é o suporte ao send_case na função select (). O que tornou o select () do SObjectizer muito mais parecido com o Go. Agora, usando select (), você pode não apenas ler mensagens de vários canais CSP, mas também enviar mensagens de saída para os canais que estavam prontos para serem gravados.


Mas, para revelar esse tópico, você precisa começar de longe.


O surgimento de elementos CSP no SObjectizer-5


Elementos do CSP, ou seja, análogos dos canais do CSP, apareceram no SObjectizer-5 não para marcar a caixa "suporte ao CSP", mas para resolver um problema prático.


O fato é que, quando todo o aplicativo é inteiramente baseado no SObjectizer, a troca de informações entre várias entidades (partes) do programa é realizada da única maneira óbvia. Tudo no aplicativo é apresentado na forma de agentes (atores) e os agentes simplesmente enviam mensagens um para o outro de maneira padrão.


Mas quando no aplicativo apenas parte da funcionalidade é implementada no SObjectizer ...


Por exemplo, um aplicativo GUI no Qt ou wxWidgets, no qual a parte principal do código é uma GUI e um SObjectizer é necessário para executar algumas tarefas em segundo plano. Ou parte do aplicativo é gravada usando threads simples e Asio, e os dados lidos pelo Asio da rede são enviados aos agentes do SObjectizer para processamento.


Quando um aplicativo tem uma parte do SObjectizer e uma parte que não é do SObjectizer, surge a pergunta: como transferir informações da parte do SObjectizer do aplicativo para a parte que não é do SObjectizer?


A solução foi encontrada na forma dos chamados cadeias de mensagens (mchains), ou seja, conversas. Que, por acaso, acabou sendo a essência dos canais CSP. A parte SObjectizer do aplicativo envia mensagens para o mchain da maneira usual, usando a função send () regular.


Para ler as mensagens da parte que não é do SObjectizer, você pode usar a nova função receive (), para a qual você não precisa criar agentes ou mergulhar em outros curingas do SObjectizer.


Acabou sendo um esquema compreensível e funcional.


Uso indevido de mchains


Além disso, o esquema acabou sendo tão compreensível e funcionando que, rapidamente, alguns aplicativos no SObjectizer começaram a escrever sem nenhum agente, apenas no ah-mchain. I.e. usando a abordagem CSP, não o modelo do ator. Já havia artigos sobre isso aqui em Habré: um e dois .


Isso levou a duas consequências interessantes.


Primeiro, a função receive () está repleta de recursos avançados. Isso foi necessário para que fosse possível fazer apenas uma chamada para receber (), cujo retorno ocorreria quando todo o trabalho necessário já estivesse feito. Aqui estão exemplos do que o receptor () do SObjectizer pode fazer:


using namespace so_5; //    3 . //  3    mchain ,   . //   receive    3 , //      . receive( from(chain).handle_n( 3 ), handlers... ); //    3 . //       mchain ,    //     200ms. // ..     200ms,    receive,   //      . receive( from(chain).handle_n( 3 ).empty_timeout( milliseconds(200) ), handlers... ); //       . //     ,    //  500ms. receive( from(chain).handle_all().empty_timeout( milliseconds(500) ), handlers... ); //       . //       2s. receive( from(chain).handle_all().total_time( seconds(2) ), handlers... ); 

Em segundo lugar, logo ficou claro que, embora vários tipos de mensagens possam ser colocadas no mchain do SObjectizer e, apesar da presença de uma função de recebimento () avançada, às vezes você precisa trabalhar com vários canais ao mesmo tempo ...


selecione () mas somente leitura


A função select () foi adicionada ao SObjectizer para ler e processar mensagens de vários mchains. O clear business select () apareceu não apenas assim, mas sob a influência do idioma Go. Mas select () do SObjectizer tinha dois recursos.


Primeiramente, nosso select (), como receive (), era orientado ao script, quando select () é chamado apenas uma vez e todo o trabalho útil é feito dentro dele. Por exemplo:


 using namespace so_5; mchain_t ch1 = env.create_mchain(...); mchain_t ch2 = env.create_mchain(...); //    3 . //    3    ch1. //  2  ch1    ch2. //    ch1  2  ch2... // //   ,       . // select()      3 , //     . select( from_all().handle_n( 3 ), receive_case( ch1, []( const first_message_type & msg ) { ... }, []( const second_message_type & msg ) { ... } ), receive_case( ch2, []( const third_message_type & msg ) { ... }, []( so_5::mhood_t< some_signal_type > ) { ... } ), ... ) ); //    3 . //    ,     200ms. select( from_all().handle_n( 3 ).empty_timeout( milliseconds(200) ), receive_case( ch1, []( const first_message_type & msg ) { ... }, []( const second_message_type & msg ) { ... } ), receive_case( ch2, []( const third_message_type & msg ) { ... }, []( so_5::mhood_t< some_signal_type > ) { ... } ), ... ) ); //       . //    ,     500ms. select( from_all().handle_all().empty_timeout( milliseconds(500) ), receive_case( ch1, []( const first_message_type & msg ) { ... }, []( const second_message_type & msg ) { ... } ), receive_case( ch2, []( const third_message_type & msg ) { ... }, []( so_5::mhood_t< some_signal_type > ) { ... } ), ... ) ); 

Em segundo lugar, select () não suportava o envio de mensagens para o canal. I.e. foi possível ler mensagens dos canais. Mas para enviar mensagens para o canal usando select () - no.


Agora é até difícil lembrar por que isso aconteceu. Provavelmente porque select () com suporte ao send_case acabou por ser uma tarefa difícil e não foram encontrados recursos para resolvê-la.


mchain no SObjectizer são mais complicados do que os canais no Go


Inicialmente, select () sem suporte ao send_case não era considerado um problema. O fato é que os mchains no SObjectizer têm suas próprias especificidades que os canais Go não possuem.


Primeiramente, as mchains do SObjectizer são divididas em sem dimensão e com uma capacidade máxima fixa. Portanto, se send () for executado para um mchain sem dimensão, esse send () não será bloqueado em princípio. Portanto, não faz sentido usar select () para enviar uma mensagem para o mchain sem dimensão.


Em segundo lugar, para mchains com capacidade máxima fixa, ao criar, indica imediatamente o que acontece quando você tenta escrever uma mensagem no mchain completo:


  • Preciso esperar pela aparência de espaço livre no mchain. E se necessário, quanto tempo;
  • se não houver espaço livre, o que fazer: excluir a mensagem mais antiga do mchain, ignorar a nova mensagem, lançar uma exceção ou até mesmo chamar std :: abort () (esse script rígido é bastante requisitado na prática).

Portanto, um cenário bastante frequente (até onde eu sei) de usar o select no Go para enviar uma mensagem que não bloqueia fortemente a goroutin estava imediatamente disponível no SObjectizer sem faíscas e sem seleção.


No final, uma seleção completa ()


No entanto, o tempo passou, ocasionalmente houve casos em que a falta de suporte send_case em select () ainda era afetada. Além disso, nesses casos, os recursos internos do mchains não ajudaram, mas o contrário.


Portanto, de tempos em tempos eu tentava abordar o problema da implementação do send_case. Mas até recentemente, nada funcionava. Principalmente porque não foi possível criar o design desse send_case em si. I.e. como deve ser o send_case dentro de select ()? O que exatamente ele deve fazer se for possível enviar? Em caso de impossibilidade? O que fazer com a divisão em mchains fixos e sem dimensão?


Só foi possível encontrar respostas para essas e outras perguntas em dezembro de 2019. Em grande parte devido a consultas com pessoas familiarizadas com o Go e que usaram os seletores de Go no trabalho real. Bem, assim que a imagem send_case finalmente tomou forma, a implementação chegou bem ali.


Então agora você pode escrever assim:


 using namespace so_5; struct Greeting { std::string text_; }; select(from_all().handle_n(1), send_case(ch, message_holder_t<Greeting>::make("Hello!"), []{ std::cout << "Hello sent!" << std::endl; })); 

O importante é que send_case em select () ignore a resposta de sobrecarga que foi definida para o mchain de destino. Portanto, no exemplo acima, ch pode ser criado com a reação abort_app ao tentar enviar uma mensagem para o canal completo. E se você tentar chamar send () simples para escrever em ch, então std :: abort () poderá ser chamado. Mas no caso de select () - e isso não acontecerá, select () esperará até que o espaço livre apareça em ch. Ou até ch ser fechado.


Aqui estão mais alguns exemplos do que o send_case pode fazer no select () do SObjectizer:


 using namespace so_5; //     ,   //    . //    . select(from_all().handle_n(1), send_case(ch1, message_holder_t<FirstMessage>::make(...), []{...}), send_case(ch2, message_holder_t<SecondMessage>::make(...), []{...}), send_case(ch3, message_holder_t<ThirdMessage>::make(...), []{...})); //     . //     ( ) //   ( ). select(from_all().handle_n(3), send_case(ch1, message_holder_t<FirstMessage>::make(...), []{...}), send_case(ch2, message_holder_t<SecondMessage>::make(...), []{...}), send_case(ch3, message_holder_t<ThirdMessage>::make(...), []{...})); //     chW. //     chW    150ms. select(from_all().handle_n(1).empty_timeout(150ms), send_case(chW, message_holder_t<Msg>::make(...), []{...})); //     chW. //  ,   chW   . select(from_all().handle_n(1).no_wait_on_empty(), send_case(chW, message_holder_t<Msg>::make(...), []{...})); //    ,      250ms. select(from_all().handle_all().total_time(250ms), send_case(ch1, message_holder_t<FirstMessage>::make(...), []{...}), send_case(ch2, message_holder_t<SecondMessage>::make(...), []{...}), send_case(ch3, message_holder_t<ThirdMessage>::make(...), []{...})); 

Naturalmente, send_case em select () pode ser usado em conjunto com receive_case:


 //          //  .       //  . select(from_all().handle_n(1), send_case(ch1, message_holder_t<FirstMsg>::make(...), []{...}), send_case(ch2, message_holder_t<SecondMsg>::make(...), []{...}), receive_case(ch3, [](...){...}), receive_case(ch4, [](...){...})); 

Portanto, agora no SObjectizer, a abordagem CSP pode ser usada, como se costuma dizer, em todos os campos. Não será pior do que em Go. Verbose, é claro. Mas não é pior :)


Podemos dizer que a longa história de adição de suporte à abordagem CSP do SObjectizer terminou.


Outras coisas importantes nesta versão


Mudança final para o github


O SObjectizer originalmente viveu e se desenvolveu no SourceForge . Um ano de comerciais desde 2006. Mas no SF.net, o desempenho do Subversion estava caindo cada vez mais, então, no ano passado, mudamos para o BitBucket e o Mercurial. Assim que fizemos isso, a Atlassian anunciou que os repositórios do Mercurial com o BitBucket seriam eliminados em breve. Portanto, desde agosto de 2019, o SObjectizer e o so5extra estão localizados no GitHub.


O SF.net tem todo o conteúdo antigo restante, incluindo o Wiki com documentação para versões anteriores do SObjectizer. E também a seção Arquivos, de onde você pode baixar arquivos de diferentes versões do SObjectizer / so5extra e não apenas (por exemplo, PDFs com algumas apresentações sobre o SObjectizer ).


Em geral, procure-nos agora no GitHub . E não se esqueça de colocar estrelas, temos muito pouco por enquanto;)


Corrigido o comportamento de mensagens envelopadas


No SO-5.7.0, ocorreu uma pequena correção que não poderia ter sido mencionada. Mas vale a pena dizer, porque esta é uma boa demonstração de como os vários recursos acumulados no SObjectizer afetam um ao outro durante seu desenvolvimento.


Há quatro anos, o suporte a agentes, que são máquinas de estado hierárquico, foi adicionado ao SObjectizer (mais detalhes aqui ). Depois de mais alguns anos, envelopes de mensagens foram adicionados ao SObjectizer. I.e. a mensagem, quando enviada, foi agrupada em um objeto de envelope adicional e esse envelope pode receber informações sobre o que está acontecendo com a mensagem.


Um dos recursos do mecanismo de mensagens envelopadas é que o envelope é informado de que a mensagem foi entregue ao destinatário. Ou seja, um manipulador para esta mensagem foi encontrado no agente do assinante e esse manipulador foi chamado.


Verificou-se que, se o agente destinatário da mensagem for uma máquina de estado hierárquica que usa um recurso como suppress() (ou seja, forçar a mensagem a ser ignorada em um estado específico), o envelope poderá receber uma notificação de entrega incorreta, embora a mensagem tenha sido realmente rejeitada pelo destinatário devido a suppress() . Uma situação ainda mais interessante foi com transfer_to_state() , porque após alterar o estado do agente receptor, o manipulador de mensagens pode ser encontrado ou pode estar ausente. Mas o envelope sobre a entrega da mensagem foi informado de qualquer maneira.


Casos muito raros, que, até onde eu sei, não foram demonstrados na prática por ninguém. No entanto, um erro de cálculo foi feito.


Portanto, no SO-5.7.0, esse ponto é aprimorado e se a mensagem for ignorada como resultado da aplicação de suppress() ou transfer_to_state() , o envelope não pensará mais que a mensagem foi entregue ao destinatário.


Biblioteca so5extra adicional altera a licença BSD-3-CLAUSE


Em 2017, começamos a criar uma biblioteca de componentes adicionais para o SObjectizer chamada so5extra . Durante esse período, a biblioteca cresceu significativamente e contém muitas coisas úteis na casa.


O So5extra foi originalmente distribuído sob uma licença dupla: GNU Affero GPL v.3 para projetos de código aberto e comercial para projetos fechados.


Agora alteramos a licença do so5extra e, a partir da versão 1.4.0, o so5extra é distribuído sob a licença BSD-3-CLAUSE. I.e. Ele pode ser usado gratuitamente, mesmo no desenvolvimento de software proprietário.


Portanto, se estiver faltando alguma coisa no SObjectizer, você pode dar uma olhada no so5extra , e se você já tiver o que precisa?


O futuro do SObjectizer


Antes de dizer algumas palavras sobre o que o SObjectizer está esperando, é necessário fazer uma digressão importante. Especialmente para aqueles que acreditam que o SObjectizer é um "desperdício de referência", "acabamento na altura dos joelhos", "laboratório do aluno", "projeção experimental que os autores abandonam quando jogam o suficiente" ... (isso é apenas parte das características que ouvimos de especialistas em da Internet nos últimos 4-5 anos).


Desenvolvo o SObjectizer há quase dezoito anos. E posso dizer com responsabilidade que ele nunca foi um projeto piloto. Esta é uma ferramenta prática que entrou em trabalho real desde a sua primeira versão no ano 2002.


Tanto eu quanto meus colegas, e as pessoas que ousaram usar e experimentar o SObjectizer, estavam convencidas muitas vezes de que o SObjectizer realmente facilita muito o desenvolvimento de alguns tipos de aplicativos C ++ multithread. Obviamente, o SObjectizer não é uma bala de prata e nem sempre pode ser usado. Mas, quando aplicável, ajuda.


A vida regularmente oferece novamente uma oportunidade de convencer-se disso. De tempos em tempos, o código multithread de outra pessoa entra em nossa atenção, no qual não havia nada semelhante ao SObjectizer e é improvável que ele apareça. Lide com esse código aqui e ali, são momentos marcantes em que o uso de atores ou canais de CSP pode tornar o código mais simples e mais confiável. Mas não, você precisa criar padrões não triviais de interação de encadeamento por meio de variáveis ​​mutex-s e condition_variables, onde no SObjectizer você pode gerenciar com um mchain, algumas mensagens e um temporizador embutido no SObjectizer. E também gaste muito tempo testando esses esquemas não triviais ...


Então o SObjectizer foi útil para nós. Atrevo-me a pensar que foi útil não apenas para nós. E o mais importante, está aqui há muito tempo e está disponível gratuitamente para todos. Ele não vai a lugar nenhum. E para onde ir para o que está no OpenSource sob uma licença permissiva? ;)


Outra coisa é que nós mesmos implementamos toda a nossa grande lista de desejos no SObjectizer. E o desenvolvimento futuro do SObjectizer será determinado não tanto pelas nossas necessidades como pelos desejos dos usuários.


Haverá esses desejos - haverá novos recursos no SObjectizer.


Não será ... Bem, então emitiremos versões corretivas de tempos em tempos e verificaremos o desempenho do SObjectizer nas novas versões dos compiladores C ++.


Portanto, se você quiser ver algo no SObjectizer, informe-nos. Se você precisar de ajuda com o SObjectizer, não hesite em entrar em contato conosco (por meio de Problemas no GitHub ou no grupo do Google ), tentaremos definitivamente ajudar.


Bem, quero agradecer aos leitores que conseguiram ler até o final deste artigo. E tentarei responder a qualquer pergunta sobre SObjectizer / so5extra, caso isso ocorra.


PS. Ficaria grato se os leitores encontrassem tempo para escrever nos comentários se era interessante / útil ler artigos sobre o SObjectizer e se eles querem fazer isso no futuro. Ou é melhor pararmos de perder tempo escrevendo esses artigos e, assim, parar de gastar o tempo dos usuários do Habr?


PPS Ou talvez alguém que considere o SObjectizer uma ferramenta não possa ser aplicado por um motivo ou outro? Seria muito interessante saber sobre isso.

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


All Articles