Quoi de neuf dans SObjectizer-5.7.0 et qu'est-ce qui attend ce projet ensuite?

SObjectizer est un framework C ++ 17 relativement petit qui vous permet d'utiliser des approches telles que Actor Model, Publish-Subscribe et Communicating Sequential Processes (CSP) dans les programmes C ++. Ce qui simplifie grandement le développement d'applications multi-threads complexes en C ++. Si le lecteur entend parler de SObjectizer pour la première fois, alors vous pouvez faire une impression de lui à partir de cette présentation , ou de cet article déjà assez ancien.


D'une manière générale, il n'y a pas autant d'outils similaires ouverts, encore vivants et en développement pour C ++. On ne peut que rappeler QP / C ++ , CAF: C ++ Actor Framework , acteur-zeta et le très jeune projet de rotor . Il y a un choix, mais pas si grand.


Récemment, une autre version "majeure" de SObjectizer est devenue disponible, où enfin une chose est apparue dont on parle depuis longtemps, et dont la mise en œuvre m'a plusieurs fois échoué. On peut dire qu'un jalon a été franchi. C'est aussi l'occasion de parler de ce qui attend SObjectizer après la sortie de la version 5.7.0.


Prise en charge de Send_case dans select ()


Ainsi, l'innovation la plus importante apparue dans la v.5.7.0 et pour laquelle la compatibilité avec la v.5.6 publiée l'année dernière (et nous ne cassons pas la compatibilité) est la prise en charge de send_case dans la fonction select (). Ce qui a rendu select () de SObjectizer beaucoup plus semblable aux sélections Go. Maintenant, en utilisant select (), vous pouvez non seulement lire les messages de plusieurs canaux CSP, mais également envoyer des messages sortants vers les canaux qui étaient prêts à être écrits.


Mais pour révéler ce sujet, il faut partir de loin.


L'émergence d'éléments CSP dans SObjectizer-5


Des éléments de CSP, à savoir des analogues de canaux CSP, sont apparus dans SObjectizer-5 non pas pour cocher la case "CSP support", mais pour résoudre un problème pratique.


Le fait est que lorsque l'application entière est entièrement basée sur SObjectizer, l'échange d'informations entre différentes entités (parties) du programme est réalisé de la seule manière évidente. Tout dans l'application est présenté sous forme d'agents (acteurs) et les agents s'envoient simplement des messages de manière standard.


Mais lorsque dans l'application, seule une partie des fonctionnalités est implémentée sur SObjectizer ...


Par exemple, une application GUI dans Qt ou wxWidgets, dans laquelle la partie principale du code est une interface graphique et un SObjectizer est nécessaire pour effectuer certaines tâches d'arrière-plan. Ou une partie de l'application est écrite à l'aide de threads nus et d'Asio, et les données lues par Asio à partir du réseau sont envoyées aux agents SObjectizer pour traitement.


Lorsqu'une application a une partie SObjectizer et une partie non SObjectizer, la question se pose: comment transférer des informations de la partie SObjectizer de l'application vers la partie non SObjectizer?


La solution a été trouvée sous la forme de soi-disant chaînes de messages (mchains), c'est-à-dire conversations. Ce qui s'est avéré être l'essence même des canaux CSP. La partie SObjectizer de l'application envoie des messages à mchain de la manière habituelle, à l'aide de la fonction send () standard.


Pour lire les messages de la partie non SObjectizer, vous pouvez utiliser la nouvelle fonction receive (), à utiliser que vous n'avez pas eu besoin de créer d'agents ou de plonger dans d'autres jokers de SObjectizer.


Il s'est avéré tout à fait compréhensible et efficace.


Utilisation abusive des chaînes


De plus, le schéma s'est avéré si compréhensible et efficace que certaines applications sur SObjectizer ont commencé à écrire sans aucun agent, uniquement sur mchain. C'est-à-dire en utilisant l'approche CSP, pas le modèle d'acteur. Il y avait déjà des articles à ce sujet ici sur Habré: un et deux .


Cela a conduit à deux conséquences intéressantes.


Tout d'abord, la fonction receive () a envahi les fonctionnalités avancées. Cela était nécessaire pour effectuer un seul appel à recevoir (), dont le retour se produirait lorsque tout le travail nécessaire a déjà été effectué. Voici des exemples de ce que la réception de SObjectizer peut faire:


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... ); 

Deuxièmement, il est vite devenu clair que même si divers types de messages peuvent être placés dans la chaîne du SObjectizer, et même malgré la présence d'une fonction de réception avancée (), vous devez parfois pouvoir travailler avec plusieurs canaux à la fois ...


select () mais en lecture seule


La fonction select () a été ajoutée à SObjectizer pour lire et traiter les messages de plusieurs chaînes. Clear business select () est apparu non seulement comme ça, mais sous l'influence du langage Go. Mais le select () de SObjectizer avait deux fonctionnalités.


Tout d'abord, notre select (), comme receive (), était orienté script, lorsque select () n'est appelé qu'une seule fois et que tout le travail utile est effectué à l'intérieur. Par exemple:


 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 > ) { ... } ), ... ) ); 

Deuxièmement, select () ne prend pas en charge l'envoi de messages au canal. C'est-à-dire Vous pouvez lire les messages des canaux. Mais pour envoyer des messages au canal en utilisant select () - non.


Maintenant, il est encore difficile de se rappeler pourquoi cela s'est produit. Probablement parce que select () avec le support send_case s'est avéré être une tâche difficile et aucune ressource n'a été trouvée pour le résoudre.


les chaînes dans SObjectizer sont plus compliquées que les chaînes dans Go


Initialement, select () sans support send_case n'était pas considéré comme un problème. Le fait est que les chaînes dans SObjectizer ont leurs propres spécificités que les canaux Go n'ont pas.


Premièrement, les chaînes SObjectizer sont divisées en sans dimension et avec une capacité maximale fixe. Par conséquent, si send () est exécuté pour une chaîne sans dimension, alors send () ne sera pas bloqué en principe. Par conséquent, cela n'a aucun sens d'utiliser select () pour envoyer un message à la chaîne sans dimension.


Deuxièmement, pour les chaînes avec une capacité maximale fixe, lors de la création, cela indique immédiatement ce qui se passe lorsque vous essayez d'écrire un message sur la chaîne complète:


  • Dois-je attendre l'apparition de l'espace libre dans mchain. Et si nécessaire, combien de temps;
  • s'il n'y a pas d'espace libre, alors que faire: supprimer le message le plus ancien de mchain, ignorer le nouveau message, lever une exception ou même appeler std :: abort () (ce script dur est assez demandé en pratique).

Ainsi, un scénario assez fréquent (pour autant que je sache) d'utilisation de select dans Go pour envoyer un message qui ne bloque pas étroitement goroutin était immédiatement disponible dans SObjectizer sans étincelles et sans select.


En fin de compte, une sélection complète ()


Néanmoins, le temps a passé, parfois il y avait des cas où le manque de support send_case dans select () était toujours affecté. De plus, dans ces cas, les capacités intégrées des mchains n'ont pas aidé, mais plutôt le contraire.


Par conséquent, de temps en temps, j'essayais d'aborder la question de la mise en œuvre de send_case. Mais jusqu'à récemment, rien ne fonctionnait. Principalement parce qu'il n'a pas été possible de concevoir le design de ce send_case lui-même. C'est-à-dire à quoi devrait ressembler send_case dans select ()? Que doit-il faire exactement s'il est possible d'envoyer? En cas d'impossibilité? Que faire de la division en chaînes fixes et sans dimension?


Il n'a été possible de trouver des réponses pour moi à ces questions et à d'autres qu'en décembre 2019. En grande partie grâce à des consultations avec des personnes qui connaissent bien le Go et qui ont utilisé les sélections Go dans le travail réel. Eh bien, dès que l'image send_case a finalement pris forme, l'implémentation est arrivée juste là.


Alors maintenant, vous pouvez écrire comme ceci:


 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; })); 

L'important est que send_case dans select () ignore la réponse de surcharge qui a été définie pour la chaîne cible. Ainsi, dans l'exemple ci-dessus, ch pourrait être créé avec la réaction abort_app lorsque vous essayez d'envoyer un message au canal complet. Et si vous essayez d'appeler simple send () pour écrire dans ch, alors std :: abort () peut être appelé. Mais dans le cas de select () - et cela ne se produira pas, select () attendra que l'espace libre apparaisse dans le ch. Ou jusqu'à ce que le ch soit fermé.


Voici quelques exemples supplémentaires de ce que send_case peut faire dans select () de 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(...), []{...})); 

Naturellement, send_case dans select () peut être utilisé en conjonction avec 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, [](...){...})); 

Alors maintenant, dans SObjectizer, l'approche CSP peut être utilisée, comme on dit, dans tous les domaines. Ce ne sera pas pire que dans Go. Verbose, bien sûr. Mais pas pire :)


Nous pouvons dire que la longue histoire de l'ajout de la prise en charge de l'approche CSP à SObjectizer est terminée.


Autres éléments importants dans cette version


Déplacement final vers github


SObjectizer a vécu et développé à l'origine chez SourceForge . Une année de publicités depuis 2006. Mais sur SF.net, les performances de Subversion diminuaient de plus en plus, donc l'année dernière nous sommes passés à BitBucket et Mercurial. Dès que nous l'avons fait, Atlassian a annoncé que les référentiels Mercurial avec BitBucket seraient bientôt supprimés. Par conséquent, depuis août 2019, SObjectizer et so5extra sont situés sur GitHub.


SF.net a tout l'ancien contenu restant, y compris le Wiki avec la documentation des versions précédentes de SObjectizer. Et aussi la section Fichiers à partir de laquelle vous pouvez télécharger des archives de différentes versions de SObjectizer / so5extra et pas seulement (par exemple, des PDF avec des présentations sur SObjectizer ).


En général, recherchez-nous maintenant sur GitHub . Et n'oubliez pas de mettre des étoiles, nous en avons trop peu pour l'instant;)


Comportement fixe des messages enveloppés


Dans SO-5.7.0, un petit correctif a eu lieu qui n'aurait pas pu être mentionné. Mais cela vaut la peine de le dire, car il s'agit d'une bonne démonstration de la façon dont les différentes fonctionnalités accumulées dans SObjectizer s'influencent mutuellement au cours de son développement.


Il y a quatre ans, la prise en charge des agents, qui sont des machines à états hiérarchiques, a été ajoutée à SObjectizer (plus de détails ici ). Ensuite, après quelques années, des enveloppes de message ont été ajoutées à SObjectizer. C'est-à-dire le message, une fois envoyé, était enveloppé dans un objet enveloppe supplémentaire et cette enveloppe pouvait recevoir des informations sur ce qui se passait avec le message.


L'une des caractéristiques du mécanisme des messages enveloppés est que l'enveloppe est informée que le message a été remis au destinataire. C'est-à-dire qu'un gestionnaire de ce message a été trouvé sur l'agent d'abonné et que ce gestionnaire a été appelé.


Il s'est avéré que si l'agent destinataire du message est une machine à états hiérarchique qui utilise une fonctionnalité telle que suppress() (c'est-à-dire forçant le message à être ignoré dans un état spécifique), l'enveloppe peut recevoir une notification de remise incorrecte, bien que le message ait été effectivement rejeté par le destinataire en raison de suppress() . Une situation encore plus intéressante était avec transfer_to_state() , car après avoir changé l'état de l'agent récepteur, le gestionnaire de messages peut être trouvé ou il peut être absent. Mais l'enveloppe concernant la remise du message a quand même été informée.


Des cas très rares qui, à ma connaissance, n'ont été mis en évidence par personne. Néanmoins, une erreur de calcul a été faite.


Par conséquent, dans SO-5.7.0, ce point est amélioré et si le message est ignoré suite à l'application de suppress() ou transfer_to_state() , l'enveloppe ne pensera plus que le message a été remis au destinataire.


La bibliothèque so5extra supplémentaire modifie la licence BSD-3-CLAUSE


En 2017, nous avons commencé à créer une bibliothèque de composants supplémentaires pour SObjectizer appelée so5extra . Pendant ce temps, la bibliothèque s'est considérablement développée et contient de nombreuses choses utiles dans le ménage.


So5extra était à l'origine distribué sous une double licence: GNU Affero GPL v.3 pour les projets open source et commercial pour les projets fermés.


Nous avons maintenant changé la licence de so5extra et à partir de la version 1.4.0, so5extra est distribué sous la licence BSD-3-CLAUSE. C'est-à-dire il peut être utilisé gratuitement même lors du développement de logiciels propriétaires.


Par conséquent, si quelque chose vous manque dans SObjectizer, vous pouvez jeter un œil à so5extra , et si vous avez déjà ce dont vous avez besoin?


L'avenir de SObjectizer


Avant de dire quelques mots sur ce que SObjectizer attend, vous devez faire une digression importante. Surtout pour ceux qui croient que SObjectizer est un «déchet de référence», une «fabrication à hauteur du genou», un «laboratoire étudiant», une «projection expérimentale que les auteurs abandonnent lorsqu'ils jouent suffisamment» ... (ce n'est qu'une partie des caractéristiques que nous avons entendues d'experts en de notre Internet au cours des 4-5 dernières années).


Je développe SObjectizer depuis près de dix-huit ans. Et je peux dire de façon responsable qu'il n'a jamais été un projet pilote. Il s'agit d'un outil pratique qui est entré dans le vrai travail depuis sa toute première version en 2002ème année.


Mes collègues et moi-même, ainsi que les personnes qui ont osé prendre et essayer SObjectizer, étions convaincus à plusieurs reprises que SObjectizer rend vraiment le développement de certains types d'applications C ++ multithread beaucoup plus facile. Bien sûr, SObjectizer n'est pas une solution miracle et ne peut en aucun cas être utilisé. Mais le cas échéant, cela aide.


La vie offre régulièrement une fois de plus l'occasion d'en être convaincu. De temps en temps, le code multithread de quelqu'un d'autre vient à notre attention, dans lequel il n'y avait rien de similaire à SObjectizer et il est peu probable qu'il apparaisse jamais. Traitez ce code ici et là, les moments sont frappants lorsque l'utilisation d'acteurs ou de canaux CSP pourrait rendre le code à la fois plus simple et plus fiable. Mais non, vous devez créer des modèles non triviaux d'interaction de threads au moyen de mutex-s et de condition_variables où dans SObjectizer vous pouvez gérer avec une chaîne, quelques messages et une minuterie intégrée à SObjectizer. Et puis passez aussi beaucoup de temps à tester ces schémas non triviaux ...


SObjectizer nous a donc été utile. J'ose penser que cela nous a été utile non seulement. Et surtout, il existe depuis longtemps et est accessible gratuitement à tous. Il ne partira nulle part. Et où aller à ce qui est dans OpenSource sous une licence permissive? ;)


Une autre chose est que nous avons nous-mêmes implémenté toute notre grande liste de souhaits dans SObjectizer. Et le développement futur de SObjectizer sera déterminé non pas tant par nos besoins que par les souhaits des utilisateurs.


Il y aura de tels souhaits - il y aura de nouvelles fonctionnalités dans SObjectizer.


Ce ne sera pas ... Eh bien, nous publierons de temps en temps des versions correctives et vérifierons les performances de SObjectizer sous les nouvelles versions des compilateurs C ++.


Donc, si vous voulez voir quelque chose dans SObjectizer, faites-le nous savoir. Si vous avez besoin d'aide avec SObjectizer, n'hésitez pas à nous contacter (via Problèmes sur GitHub ou le groupe Google ), nous essaierons certainement de vous aider.


Eh bien, je tiens à remercier ces lecteurs qui ont pu lire jusqu'à la fin de cet article. Et j'essaierai de répondre à toutes les questions concernant SObjectizer / so5extra, si cela se produit.


PS. Je serais reconnaissant aux lecteurs de trouver du temps pour écrire dans les commentaires s'il était intéressant / utile de lire des articles sur SObjectizer et s'ils souhaitent le faire à l'avenir. Ou est-il préférable pour nous d'arrêter de perdre du temps à écrire de tels articles, et donc de ne plus prendre le temps des utilisateurs Habr?


PPS Ou peut-être que quelqu'un considérait SObjectizer comme un outil ne pouvait pas être appliqué pour une raison ou une autre? Il serait très intéressant de le savoir.

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


All Articles