
Le troisième jour, une nouvelle version de SObjectizer est devenue disponible : 5.6.0 . Sa principale caractéristique est le rejet de la compatibilité avec la branche stable précédente 5.5, qui n'a cessé de se développer au cours des quatre ans et demi.
Les principes de base du fonctionnement de SObjectizer-5 sont restés les mêmes. Les communications, les agents, les coopérations et les répartiteurs sont toujours avec nous. Mais quelque chose a sérieusement changé, quelque chose a été généralement jeté. Par conséquent, le simple fait de prendre SO-5.6.0 et de recompiler votre code échouera. Quelque chose doit être réécrit. Quelque chose devra peut-être être repensé.
Pourquoi avons-nous pris soin de la compatibilité pendant plusieurs années, puis avons décidé de tout prendre et tout casser? Et qu'est-ce qui s'est cassé le plus profondément?
J'essaierai d'en parler dans cet article.
Pourquoi aviez-vous besoin de casser quelque chose?
C'est aussi simple que cela.
SObjectizer-5.5 au cours de son développement a absorbé tant de choses différentes et diverses qui n'étaient pas initialement prévues, en conséquence, il a formé trop de béquilles et d'accessoires à l'intérieur. Avec chaque nouvelle version, ajouter quelque chose de nouveau à SO-5.5 devenait de plus en plus difficile. Et enfin, à la question "Pourquoi avons-nous besoin de tout cela?" aucune réponse appropriée n'a été trouvée.
La première raison est donc la re-complication des abats de SObjectizer.
La deuxième raison est que nous sommes stupidement fatigués de nous concentrer sur les anciens compilateurs C ++. La branche 5.5 a commencé en 2014, lorsque nous avions, si je ne me trompe pas, gcc-4.8 et MSVS2013. Et à ce niveau, nous avons continué à maintenir la barre des exigences pour le niveau de support pour la norme C ++.
Au départ, nous avions un «intérêt égoïste» à cet égard. De plus, pendant un certain temps, nous avons considéré les faibles exigences de qualité de support pour la norme C ++ comme notre "avantage concurrentiel".
Mais le temps passe, "l'intérêt égoïste" est révolu. Certains avantages d'un tel "avantage concurrentiel" ne sont pas visibles. Peut-être qu'ils le seraient, si nous travaillions avec C ++ 98, alors nous serions intéressés par l'entreprise sanglante. Mais l'entreprise sanglante de ceux comme nous, en principe, n'est pas intéressée. Il a donc été décidé d'arrêter de nous limiter et de prendre quelque chose de plus frais. Nous avons donc pris le plus frais de l'écurie en ce moment: C ++ 17.
Évidemment, tout le monde n'aimera pas cette solution, après tout, pour beaucoup de C ++ 17, c'est maintenant un bord d'attaque inaccessible, qui est encore très, très loin.
Néanmoins, nous avons décidé d'un tel risque. Tout de même, le processus de vulgarisation de SObjectizer ne va pas vite, donc quand SObjectizer devient plus ou moins largement sollicité, C ++ 17 ne sera plus un «bord d'attaque». Au lieu de cela, il sera traité de la même manière que maintenant en C ++ 11.
En général, au lieu de continuer à construire des béquilles en utilisant un sous-ensemble de C ++ 11, nous avons décidé de refaire sérieusement les internes de SObjectizer en utilisant C ++ 17. Construire une base sur laquelle SObjectizer pourra progressivement se développer au cours des quatre ou cinq prochaines années.
Qu'est-ce qui a sérieusement changé dans SObjectizer-5.6?
Passons maintenant brièvement en revue certains des changements les plus frappants.
Les coopérations d'agent n'ont plus de nom de chaîne
Le problème
Dès le début, SObjectizer-5 a exigé que chaque coopération ait son propre nom de chaîne unique. Cette fonctionnalité a été héritée du cinquième SObjectizer du précédent, quatrième SObjectizer.
Par conséquent, SObjectizer devait stocker les noms des coopérations enregistrées. Vérifiez leur caractère unique lors de l'inscription. Recherche de coopération par nom lors de la désinscription, etc., etc.
Depuis les toutes premières versions, un schéma simple a été utilisé dans SObjectzer-5: un dictionnaire unique de coopérations enregistrées protégées par mutex. Lors de l'enregistrement d'une coopération, mutex est capturé, l'unicité du nom de la coopération, la présence d'un parent, etc. sont vérifiées. Après vérification, le dictionnaire est modifié, après quoi le mutex est libéré. Cela signifie que si en même temps l'enregistrement / le désenregistrement de plusieurs coopérations commence à la fois, alors à certains moments, ils s'arrêteront et attendront que l'une des opérations se termine avec le dictionnaire coopératif. Pour cette raison, les opérations coopératives n'ont pas bien évolué.
C'est ce dont je voulais me débarrasser pour améliorer la situation avec la rapidité d'enregistrement des coopérations.
Solution
Deux façons principales de résoudre ce problème ont été envisagées.
Premièrement, le stockage des noms de chaîne, mais la modification de la façon dont le dictionnaire est stocké afin que l'opération d'enregistrement de coopération puisse évoluer. Par exemple, le partage de dictionnaire, c.-à-d. le briser en plusieurs morceaux, dont chacun serait protégé par son mutex.
Deuxièmement, un rejet complet des noms de chaîne et l'utilisation de certains identifiants attribués par SObjectizer.
En conséquence, nous avons choisi la deuxième méthode et abandonné complètement la dénomination des coopératives. Maintenant, dans SObjectizer, il existe une chose telle que coop_handle
, c'est-à-dire un handle dont le contenu est caché à l'utilisateur, mais qui peut être comparé à std::weak_ptr
.
SObjectizer renvoie coop_handle
lors de l'enregistrement d'une collaboration:
auto coop = env.make_coop(); ...
Cette poignée doit être utilisée pour le retrait de la coopération:
auto coop = env.make_coop(); ...
En outre, cette poignée doit être utilisée lors de l'établissement d'une relation parent-enfant:
La structure du référentiel de coopération au sein de l'environnement SObjectizer a également radicalement changé. Si, avant la version 5.5 incluse, il s'agissait d'un dictionnaire commun, chaque coopération est désormais un référentiel de liens vers les coopérations enfants. C'est-à-dire les coopératives forment un arbre avec une racine dans une coopérative racine spéciale cachée à l'utilisateur.
Une telle structure permet de mieux redimensionner register_coop
opérations register_coop
et deregister_coop
: le blocage mutuel des opérations parallèles ne se produit que si elles appartiennent toutes deux à la même coopération parentale. Pour plus de clarté, voici le résultat du lancement d'un benchmark spécial qui mesure les performances des opérations avec les coopérations sur mon ancien portable avec Ubuntu 16.04 et 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
C'est-à-dire la version 5.6.0 a géré près de 2 millions de coopérations en ~ 15,5 secondes.
Et voici la version 5.5.24.4, la dernière de la branche 5.5 pour le moment:
_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
Le même scénario, mais le résultat est trois fois pire.
Il ne reste qu'un seul type de répartiteurs
Les répartiteurs sont l'une des pierres angulaires de SObjectizer. Ce sont les répartiteurs qui déterminent où et comment les agents traiteront leurs messages. Donc, sans l'idée de répartiteurs, il n'y aurait probablement pas eu de SObjectizer.
Cependant, les répartiteurs eux-mêmes ont évolué, évolué et évolué au point qu'il n'était même pas difficile pour nous de créer un nouveau répartiteur pour SObjectizer-5.5. Mais très gênant. Cependant, prenons-le dans l'ordre.
Initialement, tous les répartiteurs dont l'application avait besoin ne pouvaient être créés qu'au démarrage de SObjectizer:
so_5::launch( []( so_5::environment_t & env ) { },
Je n'ai pas créé le manager nécessaire avant le départ - tout, c'est ma faute, vous ne pouvez rien changer.
Il est clair que cela n'est pas pratique et à mesure que les scénarios d'utilisation de SObjectizer se développaient, il était nécessaire de résoudre ce problème. Par conséquent, la méthode add_dispatcher_if_not_exists
, qui a vérifié la présence du répartiteur et, s'il n'y en avait pas, a permis de créer une nouvelle instance:
so_5::launch( []( so_5::environment_t & env ) { ...
Ces répartiteurs étaient appelés publics. Les répartiteurs publics avaient des noms uniques. Et en utilisant ces noms, les agents étaient liés aux répartiteurs:
so_5::launch( []( so_5::environment_t & env ) { ...
Mais les répartiteurs publics avaient une caractéristique désagréable. Ils ont commencé à travailler immédiatement après avoir été ajoutés à l'environnement SObjectizer et ont continué à travailler jusqu'à ce que l'environnement SObjectizer termine son travail.
Encore une fois, au fil du temps, il a commencé à interférer. Il fallait veiller à ce que des répartiteurs puissent être ajoutés selon les besoins et que les répartiteurs devenus inutiles soient automatiquement supprimés.
Il y avait donc des répartiteurs «privés». Ces répartiteurs n'avaient pas de nom et vivaient tant qu'il y avait des références à eux. Des répartiteurs privés pouvaient être créés à tout moment après le démarrage de l'environnement SObjectizer, ils étaient détruits automatiquement.
En général, les répartiteurs privés se sont avérés être un maillon très réussi dans l'évolution des répartiteurs, mais travailler avec eux était très différent de travailler avec des répartiteurs publics:
so_5::launch( []( so_5::environment_t & env ) { ...
Encore plus de répartiteurs privés et publics différaient dans la mise en œuvre. Par conséquent, afin de ne pas dupliquer le code et d'écrire séparément le répartiteur public et privé du même type, j'ai dû utiliser des constructions plutôt complexes avec des modèles et l'héritage.
En conséquence, j'étais fatigué d'accompagner toute cette variété, et dans SObjectizer-5.6, il ne restait qu'un seul type de répartiteurs. En fait, c'est un analogue des répartiteurs privés. Mais seulement sans mention explicite du mot "privé". Alors maintenant, le fragment montré ci-dessus sera écrit comme suit:
so_5::launch( []( so_5::environment_t & env ) { ...
Il n'y a que des fonctions libres send, send_delayed et send_periodic left
Le développement de l'API pour l'envoi de messages à SObjectizer est probablement l'exemple le plus frappant de la façon dont SObjectizer a changé car la prise en charge de C ++ 11 s'est améliorée dans les compilateurs à notre disposition.
Tout d'abord, les messages ont été envoyés comme ceci:
mbox->deliver_message(new my_message(...));
Ou, si vous suivez les "recommandations des meilleurs éleveurs de chiens" (c):
std::unique_ptr<my_message> msg(new my_message(...)); mbox->deliver_message(std::move(msg));
Cependant, nous avons mis à notre disposition des compilateurs avec prise en charge des modèles variadiques et des fonctions d'envoi. Il est devenu possible d'écrire comme ceci:
send<my_message>(target, ...);
Certes, il a fallu plus de temps à toute une famille pour se send_to_agent
partir d'un simple send
, y compris send_to_agent
, send_delayed_to_agent
, etc. Et puis, pour rendre cette famille plus étroite à l'ensemble familier de send
, send_delayed
et send_periodic
.
Mais, malgré le fait que la famille des fonctions d'envoi a été formée il y a assez longtemps et est le moyen recommandé pour envoyer des messages depuis plusieurs années, les anciennes méthodes telles que deliver_message
, schedule_timer
et single_timer
étaient toujours disponibles pour l'utilisateur.
Mais dans la version 5.6.0, seules les fonctions free send
, send_delayed
et send_periodic
étaient enregistrées dans l'API publique SObjectizer. Tout le reste a été soit complètement supprimé, soit transféré dans les espaces de noms SObjectizer internes.
Donc, dans SObjectizer-5.6, l'interface d'envoi de messages est finalement devenue ce qu'elle aurait été si nous avions des compilateurs avec un support C ++ 11 normal dès le début. Eh bien, en plus de cela, si nous avions l'expérience de l'utilisation de ce C ++ 11 très normal.
Avec les fonctions send_delayed
et send_periodic
dans les versions précédentes de SObjectizer, il y a eu un autre incident.
Pour utiliser le minuteur, vous devez avoir accès à l'environnement SObjectizer. À l'intérieur de l'agent, il existe un lien vers l'environnement SObjectizer. Et à l'intérieur de mchain, il y a un tel lien. Mais à l'intérieur de la mbox, elle n'était pas là. Par conséquent, si un message en attente a été envoyé à un agent ou à mchain, l'appel send_delayed
à:
send_delayed<my_message>(target_agent, pause, ...); send_delayed<my_message>(target_mchain, pause, ...);
Pour le cas de mbox, nous avons dû prendre un lien vers l'environnement SObjectizer ailleurs:
send_delayed<my_message>(this->so_environment(), target_mbox, pause, ...);
Cette fonctionnalité de send_delayed
et send_periodic
était un éclat mineur. Ce qui n'est pas tellement gênant, mais assez ennuyeux. Et tout cela parce qu'au départ, nous n'avions pas commencé à stocker le lien vers l'environnement SObjectizer dans mbox-ahs.
La violation de la compatibilité avec les versions précédentes était une bonne raison de se débarrasser de cet éclat.
Vous pouvez maintenant découvrir à partir de mbox pour quel environnement SObjectizer il a été créé. Et cela a permis d'utiliser les send_delayed
simples send_delayed
et send_periodic
pour tout type de destinataire de message de temporisation:
send_delayed<my_message>(target_agent, pause, ...); send_delayed<my_message>(target_mchain, pause, ...); send_delayed<my_message>(target_mbox, pause, ...);
Dans le sens littéral, "une bagatelle, mais agréable."
Plus d'agents ad hoc
Comme le dit le proverbe, "Chaque accident a un prénom, un deuxième prénom et un nom de famille." Dans le cas des agents ad hoc, voici mon prénom, mon deuxième prénom et mon nom de famille :(
Le point est le suivant. Lorsque nous avons commencé à parler de SObjectizer-5 en public, nous avons entendu beaucoup de reproches concernant la verbosité du code des exemples de SObjectizer. Et personnellement, cette verbosité m'a semblé un problème sérieux que je dois sérieusement affronter.
Une source de verbosité est la nécessité pour les agents d'hériter du type de base spécial agent_t
. Et de cela, semble-t-il, il n'y a pas d'échappatoire. Ou pas?
Il y avait donc des agents ad hoc, c'est-à-dire agents, pour la détermination desquels il n'était pas nécessaire d'écrire une classe distincte, il suffisait seulement de régler la réaction aux messages sous la forme de fonctions lambda. Par exemple, l'exemple classique de ping-pong sur les agents ad-hoc pourrait être écrit comme ceci:
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 ); } );
C'est-à-dire pas de cours à part. Nous appelons juste define_agent()
sur la coopération et obtenons une sorte d'objet agent, auquel vous pouvez vous abonner aux messages entrants.
Donc, dans SObjectizer-5, il y avait une séparation en agents réguliers et ad hoc.
Ce qui n'a pas apporté de bonus visibles, seulement les coûts de main-d'œuvre supplémentaires pour accompagner une telle séparation. Et au fil du temps, il est devenu clair que les agents ad hoc sont comme une valise sans poignée: c'est difficile à transporter et c'est dommage de partir. Mais en travaillant sur SObjectizer-5.6, il a été décidé de quitter.
Dans le même temps, une autre leçon a également été apprise, peut-être encore plus importante: dans toute discussion publique sur l'outil sur Internet, un grand nombre de personnes participeront qui sont indifférents à ce qu'est l'outil, pourquoi il est nécessaire, pourquoi il est tel qu'il est censé être utilisé, etc. Il est simplement important pour eux d'exprimer leur forte opinion. Dans le segment Internet de langue russe, en plus de cela, il est toujours très important de dire aux développeurs de l'outil comment ils sont stupides et sans instruction, et combien le résultat de leur travail n'est pas nécessaire.
Par conséquent, vous devez être très prudent dans ce que l'on vous dit. Et vous pouvez écouter (puis attentivement) uniquement ce qui est dit ici dans cette veine: "J'ai essayé de le faire sur votre instrument et je n'aime pas la quantité de code qu'il contient ici." Même ces souhaits doivent être traités avec beaucoup de soin: "Je prendrais votre développement si cela était plus facile ici et ici."
Malheureusement, la compétence de «filtrage» déclarée par les «sympathisants» sur Internet il y a environ cinq ans était bien moindre qu'aujourd'hui. Par conséquent, une telle expérience spécifique en tant qu'agents ad-hoc dans SObjectizer.
SObjectizer-5.6 ne prend plus en charge l'interaction d'agent synchronisé
Le sujet de l'interaction synchronisée entre les agents est très ancien et douloureux.
Cela a commencé à l'époque de SObjectizer-4. Et dans SObjectizer-5 a continué. Jusqu'à présent, enfin, le soi-disant demandes de service . Ce qui au départ, certes, était effrayant comme la mort. Mais j'ai réussi à leur donner un look plus ou moins décent .
Mais cela s'est avéré être le cas même lorsque la première crêpe est sortie grumeleuse :(
Dans SObjectizer, j'ai dû implémenter la livraison et le traitement des messages réguliers d'une manière, et la livraison et le traitement des demandes synchrones d'une autre. Il est particulièrement triste que ces fonctionnalités aient dû être prises en compte, y compris lors de l'implémentation de vos propres mbox.
Et après que la fonctionnalité des messages d'enveloppe a été ajoutée à SObjectizer, il est devenu nécessaire de regarder encore plus souvent et encore plus attentivement les différences entre les messages réguliers et les demandes synchrones.
En général, avec les demandes synchrones pendant la maintenance / le développement de SObjectizer, il y avait trop de maux de tête. À tel point qu'au début, il y avait une volonté concrète de se débarrasser de ces demandes très synchrones . Et puis ce désir s'est réalisé.
Ainsi, dans SObjectizer-5.6, les agents peuvent à nouveau interagir uniquement via des messages asynchrones.
Et comme parfois une interaction synchrone est toujours nécessaire, la prise en charge de ce type d'interaction a été soumise au projet so5extra qui l'accompagne :
C'est-à-dire Maintenant, travailler avec des requêtes synchrones est fondamentalement différent en ce sens que le gestionnaire de requêtes ne renvoie pas de valeur de la méthode du gestionnaire, comme c'était le cas auparavant. Au lieu de cela, la méthode make_reply
est make_reply
.
La nouvelle implémentation est bonne dans la mesure où la demande et la réponse sont envoyées à l'intérieur du SObjectizer comme des messages asynchrones réguliers. En fait, make_reply
est une implémentation légèrement plus spécifique de send
.
Et, plus important encore, la nouvelle implémentation nous a permis d'obtenir des fonctionnalités qui étaient auparavant inaccessibles:
- les requêtes synchrones (c'est-à-dire les objets
request_reply_t<Request, Reply>
) peuvent maintenant être enregistrées et / ou transmises à d'autres gestionnaires. Qu'est-ce qui permet de mettre en œuvre différents schémas d'équilibrage de charge; - Vous pouvez faire en sorte que la réponse à la demande apparaisse dans une mbox régulière de l'agent initiant la demande. Et l'agent initiateur traitera la réponse de la manière habituelle, comme tout autre message;
- Vous pouvez envoyer plusieurs demandes à différents destinataires à la fois, puis analyser leurs réponses dans l'ordre dans lequel elles ont été reçues:
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>;
Donc, nous pouvons dire qu'avec l'interaction synchrone dans SObjectizer, les événements suivants se sont produits:
- il a longtemps disparu pour des raisons idéologiques;
- ensuite il a été ajouté et il s'est avéré que cette interaction est parfois utile;
- mais l'expérience a montré que la première mise en œuvre n'est pas très réussie;
- l'ancienne implémentation a été complètement abandonnée et une nouvelle implémentation a été proposée en retour.
Ils ont travaillé sur leurs propres erreurs, en général.
Conclusion
Cet article, assez brièvement, a parlé de plusieurs changements dans SObjectizer-5.6.0 et les raisons de ces changements.
Une liste plus complète des changements peut être trouvée ici .
En conclusion, je voudrais offrir à ceux qui n'ont pas encore essayé SObjectizer, prenez-le et essayez-le. Et partagez avec nous vos sentiments: ce que vous avez aimé, ce que vous n'avez pas aimé, ce qui manquait.
Nous écoutons attentivement tous les commentaires / suggestions constructifs. De plus, ces dernières années, seul ce dont quelqu'un a besoin est inclus dans SObjectizer. Donc, si vous ne nous dites pas ce que vous aimeriez avoir dans SObjectizer, cela n'apparaîtra pas. Et si vous me le dites, alors qui sait ...;)
Le projet vit et se développe maintenant ici . Et pour ceux qui ont l'habitude d'utiliser uniquement GitHub, il existe un miroir GitHub . Ce miroir est complètement nouveau, vous pouvez donc ignorer le manque d'étoiles.
PS. Vous pouvez suivre les actualités liées à SObjectizer dans ce groupe Google . Là, vous pouvez soulever des problèmes liés à SObjectizer.