
Quelques mots sur SObjectizer et son histoire
SObjectizer est un framework C ++ plutôt petit qui simplifie le développement d'applications multithread. SObjectizer permet à un développeur d'utiliser des approches issues des modèles Actor, Publish-Subscribe et Communicating Sequential Processes (CSP). Il s'agit d'un projet OpenSource distribué sous licence BSD-3-CLAUSE.
SObjectizer a une longue histoire. SObjectizer lui-même est né en 2002 en tant que projet SObjectizer-4. Mais il était basé sur les idées du précédent Objectizer SCADA qui avait été développé entre 1995 et 2000. SObjectizer-4 était open source en 2006, mais son évolution a été arrêtée peu de temps après. Une nouvelle version de SObjectizer avec le nom SObjectizer-5 a été lancée en 2010 et a été open source en 2013. L'évolution de SObjectizer-5 est toujours en cours et SObjectizer-5 a incorporé de nombreuses nouvelles fonctionnalités depuis 2013.
SObjectizer est plus ou moins connu dans le segment russe d'Internet, mais presque inconnu en dehors de l'exUSSR. C'est parce que le SObjectizer a été principalement utilisé pour des projets locaux dans les pays exUSSR et de nombreux articles, présentations et discussions sur SObjectizer sont en russe.
Le multithreading est utilisé aussi bien en informatique parallèle qu'en informatique concurrente . Mais il y a une grande différence entre l'informatique parallèle et simultanée. Et, en conséquence, il existe des outils ciblés pour l'informatique parallèle, et il existe des outils pour l'informatique concurrente, et ils sont différents.
En gros, l'informatique parallèle consiste à utiliser plusieurs cœurs pour réduire les temps de calcul. Par exemple, le transcodage d'un fichier vidéo d'un format à un autre peut prendre une heure sur un cœur de processeur, mais seulement 15 minutes sur quatre cœurs de processeur. Des outils comme OpenMP, Intel TBB, HPX ou cpp-taskflow sont conçus pour être utilisés dans l'informatique parallèle. Et ces outils prennent en charge les approches appropriées pour ce domaine, comme la programmation basée sur les tâches ou le flux de données.
L'informatique concurrente consiste à traiter de nombreuses tâches (probablement différentes) en même temps. Le serveur de base de données ou le courtier MQ peuvent être de bons exemples: un serveur doit accepter une connexion, lire et analyser les données des connexions acceptées, gérer les demandes reçues (effectuer plusieurs actions pour chaque demande), envoyer des réponses, etc. À strictement parler, il n'est pas nécessaire d'utiliser le multithreading dans le calcul simultané: toutes ces tâches peuvent être effectuées sur un seul thread de travail. Mais l'utilisation du multithreading et de plusieurs cœurs de processeur peut rendre votre application plus performante, évolutive et réactive.
Des approches comme Actor Model ou CSP sont conçues pour traiter de l'informatique concurrente. Le projet InfineSQL et Yandex Message-Queue sont de bons exemples d'utilisation des acteurs dans le domaine de l'informatique concurrente. Ces deux projets utilisent des acteurs à l'intérieur.
Les outils tels que SObjectizer, QP / C ++ ou CAF, qui prennent en charge le modèle d'acteur, sont utiles pour résoudre les tâches de la zone de calcul simultané. Cela signifie que l'utilisation de SObjectizer ne vous donnera probablement rien dans des tâches telles que la conversion de flux vidéo. Mais vous pouvez obtenir un résultat très différent en implémentant un courtier de messages au-dessus de SObjectizer.
Clause de non-responsabilité
L'utilisation de modèles Actor ou CSP peut vous apporter d'énormes avantages dans certaines tâches, mais rien ne garantit que ces modèles conviennent à votre problème particulier. Le discours sur l'applicabilité des modèles Actor ou CSP dépasse le cadre de cet article. Supposons que le modèle Actor ou / et CSP s'applique à vos tâches et que vous savez comment les utiliser efficacement.
Quel SObjectizer peut apporter à un utilisateur?
Des principes de partage de rien et d'incendie et d'oubli tout juste sortis de la boîte
L'utilisation des acteurs suppose l'absence de données partagées. Chaque acteur possède ses données et ces données ne sont visibles par personne d'autre. C'est un principe de partage rien qui est bien connu dans le développement d'applications distribuées, par exemple. Dans une application multithread, le principe du partage de rien a un avantage important: il permet d'éviter des problèmes dangereux pour le travail avec des données partagées comme les blocages et les courses de données.
L'interaction entre les acteurs (agents) dans SObjectizer est effectuée uniquement via des messages asynchrones. Un agent envoie un message à un autre agent et cette opération ne bloque pas l'expéditeur (dans un cas courant).
L'interaction asynchrone permet d'utiliser un autre principe utile: tirer et oublier . Lorsqu'un agent a besoin d'une opération, il envoie (déclenche) un message et continue son travail. Dans la plupart des cas, le message sera reçu et traité.
Par exemple, il peut y avoir un agent qui lit les connexions acceptées et analyse les données entrantes. Si l'ensemble de la PDU est lu et analysé, l'agent envoie simplement cette PDU à un autre agent-processeur et revient à la lecture / analyse de nouvelles données entrantes.
Répartiteurs
Les répartiteurs sont l'une des pierres angulaires de SObjectizer. Les répartiteurs fournissent un contexte de travail (aka thread de travail) sur lequel un agent traitera les messages entrants. Au lieu de créer manuellement des threads de travail (ou des pools de threads), un utilisateur crée des répartiteurs et leur lie des agents. Un utilisateur peut créer autant de répartiteurs dans une application qu'il le souhaite.
La meilleure chose avec les répartiteurs et les agents dans SObjectizer est la séparation des concepts: les répartiteurs sont responsables de la gestion du contexte de travail et des propres files d'attente de messages, les agents exécutent la logique d'application et ne se soucient pas du contexte de travail. Il permet de déplacer un agent d'un répartiteur à un autre littéralement par un clic. Hier, un agent a travaillé sur le répartiteur one_thread, aujourd'hui nous pouvons le relier au répartiteur active_obj, et demain nous pouvons le relier au répartiteur thread_pool. Sans changer de ligne dans l'implémentation de l'agent.
Il existe huit types de répartiteurs dans SObjectizer-5.6.0 (et un autre peut être trouvé dans le projet compagnon so5extra): des très simples (one_thread ou thread_pool) aux plus sophistiqués (comme adv_thread_pool ou prio_dedicated_threads :: one_per_prio). Et un utilisateur peut écrire son propre répartiteur pour des conditions spécifiques.
Les machines à états hiérarchiques sont des fonctionnalités intégrées
Les agents (acteurs) dans SObjectizer sont des machines à états: la réaction sur un message entrant dépend de l'état actuel de l'agent. SObjectizer prend en charge la plupart des fonctionnalités des machines à états hiérarchiques (HSM): états imbriqués, historique profond et peu profond pour un état, gestionnaires on_enter / on_exit, limites de temps pour rester dans un état. Seuls les états orthogonaux ne sont pas pris en charge dans SObjectizer maintenant (nous n'avons pas vu la nécessité de cette fonctionnalité dans nos projets, et personne ne nous a demandé d'ajouter la prise en charge de cette fonctionnalité).
Canaux de type CSP juste sortis de leur boîte
Il n'est pas nécessaire d'utiliser les agents de SObjectizer (alias acteurs). L'application entière peut être développée simplement en utilisant des objets std::thread
et les chaînes de SObjectizer (aussi appelées canaux CSP) . Dans ce cas, le développement d'applications avec SObjectizer sera quelque peu similaire au développement en langage Go (y compris un analogue de la construction select
de Go qui permet d'attendre les messages de plusieurs canaux).
Les chaînes de SObjectizer peuvent avoir une caractéristique très importante: un mécanisme de contre-pression intégré. Si un utilisateur crée une chaîne de taille limitée et essaie ensuite de pousser un message dans la chaîne complète, l'opération d'envoi peut bloquer l'expéditeur pendant un certain temps. Il permet de résoudre un problème célèbre avec un producteur rapide et un consommateur lent.
Les mchains de SObjectizer ont une autre caractéristique intéressante: une mchain peut être utilisée comme un outil de distribution de charge très simple. Plusieurs threads peuvent attendre la réception de la même chaîne en même temps. Si un nouveau message est envoyé à cette chaîne, un seul thread lira et traitera ce message.
Seule une partie d'une application peut utiliser SObjectizer
Il n'est pas nécessaire d'utiliser SObjectizer dans chaque partie d'une application. Seule une partie d'une application peut être développée à l'aide de SObjectizer. Donc, si vous utilisez déjà Qt ou wxWidgets ou Boost.Asio comme framework principal pour votre application, il est possible d'utiliser SObjectize dans un seul sous-module de votre application.
Nous avions de l'expérience sur l'utilisation de SObjectizer pour le développement de bibliothèques qui masquent l'utilisation de SObjectizer comme détail d'implémentation. L'API publique de ces bibliothèques n'a pas du tout révélé la présence de SObjectizer. SObjectizer était entièrement sous le contrôle d'une bibliothèque: la bibliothèque a démarré et arrêté SObjectizer selon ses besoins. Ces bibliothèques ont été utilisées dans des applications qui ignoraient totalement la présence de SObjectizer.
Si SObjectizer n'est utilisé que dans une partie d'une application, il y a une tâche de communication entre les parties SObjectizer et non SObjectizer de l'application. Cette tâche est facilement résolue: les messages d'une partie non SObjectizer vers une partie SObjectizer peuvent être envoyés via le mécanisme ordinaire de remise de messages SObjectizer. Les messages dans la direction opposée peuvent être transmis via des chaînes.
Vous pouvez exécuter plusieurs instances de SObjectizer en même temps
SObjectizer permet d'exécuter plusieurs instances de SObjectizer (appelées SObjectizer Environment) dans une même application en même temps. Chaque environnement SObjectizer sera indépendant des autres environnements de ce type.
Cette fonctionnalité est inestimable dans les situations où vous devez créer une application à partir de plusieurs modules indépendants. Certains modules peuvent utiliser SObjectizer, d'autres non. Les modules qui nécessitent SObjectizer peuvent exécuter sa copie de SObjectizer Environment et qui n'auront pas d'influence sur les autres modules de l'application.
Les minuteries font partie de SObjectizer
La prise en charge des minuteries sous forme de messages retardés et périodiques est une autre des pierres angulaires de SObjectizer. SObjectizer possède plusieurs implémentations de mécanismes de temporisation (timer_wheel, timer_heap et timer_list) et peut gérer des dizaines, des centaines et des milliers de millions de temporisateurs dans une application. Un utilisateur peut choisir le mécanisme de temporisation le plus approprié pour une application. De plus, un utilisateur peut fournir sa propre implémentation de timer_thread / timer_manager si aucun des standards n'est approprié aux conditions de l'utilisateur.
SObjectizer dispose de divers points de personnalisation et d'options de réglage
SObjectizer permet la personnalisation de plusieurs mécanismes importants. Par exemple, un utilisateur peut sélectionner l'une des implémentations standard de timer_thread (ou timer_manager). Ou peut fournir sa propre implémentation. Un utilisateur peut sélectionner une implémentation d'objets de verrouillage utilisés par les files d'attente de messages dans les répartiteurs de SObjectizer. Ou peut fournir sa propre implémentation.
Un utilisateur peut implémenter son propre répartiteur. Un utilisateur peut implémenter sa propre boîte de message. Un utilisateur peut implémenter sa propre enveloppe de message. Un utilisateur peut implémenter son propre event_queue_hook. Et ainsi de suite.
Où SObjectizer peut ou ne peut pas être utilisé?
Il est beaucoup plus facile de dire où SObjectizer ne peut pas être utilisé pour des raisons objectives. Nous commençons donc la discussion en énumérant ces zones, puis nous donnerons quelques exemples de l'utilisation de SObjectizer dans le passé (et pas seulement dans le passé).
Où SObjectizer ne peut pas être utilisé?
Comme cela a été dit ci-dessus, les modèles Actor et CSP ne sont pas un bon choix pour le calcul haute performance et d'autres domaines du calcul parallèle. Donc, si vous devez plusieurs matrices ou transcoder des flux vidéo, des outils comme OpenMP, Intel TBB, cpp-taskflow, HPX ou MPI conviendront mieux.
Systèmes durs en temps réel
Malgré le fait que SObjectizer ait ses racines dans les systèmes SCADA, l'implémentation actuelle de SObjectizer (aka SObjectizer-5) ne peut pas être utilisée dans les systèmes durs en temps réel. C'est principalement en raison de l'utilisation de la mémoire dynamique dans la mise en œuvre de SObjectizer: les messages sont des objets alloués dynamiquement (cependant, SObjectizer peut utiliser des objets préalloués comme messages), les répartiteurs utilisent la mémoire dynamique pour les files d'attente de messages, même les délais pour les états de l'agent utilisent des objets alloués dynamiquement pour effectuer un contrôle horaire.
Malheureusement, le terme «temps réel» est largement surutilisé dans le monde moderne. On parle souvent de services Web en temps réel, comme «application Web en temps réel» ou «analyse Web en temps réel», etc. Le terme "en ligne" ou "en direct" est plus approprié pour de telles applications que le terme "en temps réel", même sous une forme "douce en temps réel". Ainsi, si nous parlons de quelque chose comme "une application Web en temps réel", SObjectizer peut facilement être utilisé dans de tels systèmes "en temps réel".
Systèmes embarqués contraints
SObjectizer s'appuie sur la bibliothèque standard C ++: std::thread
est utilisé pour la gestion des threads, std::atomic
, std::mutex
, std::condition_variable
sont utilisés pour la synchronisation des données, RTTI et dynamic_cast
sont utilisés à l'intérieur de SObjectizer (par exemple , std::type_index
sont utilisés pour l'identification du type de message), les exceptions C ++ sont utilisées pour le rapport d'erreurs.
Cela signifie que SObjectizer ne peut pas être utilisé dans des environnements où de telles fonctionnalités de la bibliothèque standard ne sont pas disponibles. Par exemple, dans le développement de systèmes embarqués contraints où seule une partie de C ++ et C ++ stdlib peut être utilisée.
Où SObjectizer était utilisé dans le passé?
Maintenant, nous essayons de parler brièvement de certains cas d'utilisation de l'utilisation de SObjectizer dans le passé (et pas seulement dans le passé). Malheureusement, ce ne sont pas des informations complètes car il y a des problèmes.
Tout d'abord, nous ne connaissons pas tous les usages de SObjectizer. SObjectizer est un logiciel gratuit qui peut être utilisé même dans des projets propriétaires. Donc, certaines personnes obtiennent simplement SObjectizer et l'utilisent sans fournir de commentaires pour nous. Parfois, nous acquérons des informations sur l'utilisation de SObjectizer (mais sans aucun détail), parfois nous ne savons rien.
Le deuxième problème est la permission de partager des informations sur l'utilisation de SObjectizer dans un projet particulier. Nous avons reçu cette autorisation très rarement, dans la plupart des cas, les utilisateurs de SObjectizer ne veulent pas ouvrir les détails de mise en œuvre de leurs projets (parfois nous comprenons les raisons, parfois non).
Nous nous excusons du fait que les informations fournies semblent si rares et ne contiennent aucun détail. Néanmoins, il existe quelques exemples d'utilisation de SObjectizer:
- Passerelle d'agrégation SMS / USSD qui gère plus de 500 millions de messages par mois;
- une partie du système servant aux paiements en ligne via les distributeurs automatiques de billets d'une des plus grandes banques russes;
- modélisation par simulation des processus économiques (dans le cadre de la recherche doctorale);
- acquisition de données distribuées et système analytique. Données collectées sur des points répartis dans le monde entier par les commandes du nœud central. MQTT a été utilisé comme moyen de transport pour le contrôle et la distribution des données acquises;
- environnement de test pour vérifier le système de contrôle en temps réel des équipements ferroviaires;
- système de contrôle automatique des décors de théâtre. Plus de détails peuvent être trouvés ici ;
- composants de la plateforme de gestion des données dans un système de publicité en ligne.
Un avant-goût de SObjectizer
Voyons quelques exemples simples pour prendre un avant-goût de SObjectizer. Ce sont des exemples très simples qui, nous l'espérons, ne nécessitent pas d'explications supplémentaires à l'exclusion des commentaires dans le code.
L'exemple traditionnel "Hello, World" dans le style de l'acteur modèle
L'exemple le plus simple avec un seul agent qui réagit au message hello
et termine son travail:
#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!"); }); }
Une autre version de "Hello, World" avec des agents et un modèle Publish / Subscribe
L'exemple le plus simple avec plusieurs agents, tous réagissent à la même instance de message 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!"); }); }
Si nous exécutons cet exemple, nous pouvons recevoir quelque chose comme ça:
Alice: greeting received: Hello, World! Bob: greeting received: Hello, World! Mike: greeting received: Hello, World!
Exemple "Hello, World" en style CSP
Regardons un exemple de SObjectizer sans aucun acteur, juste std::thread
et des canaux de type CSP.
Version très simple
Il s'agit d'une version très simple qui ne présente pas d'exception:
#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(); }
Version plus robuste, mais toujours simple
Il s'agit d'une version modifiée de l'exemple ci-dessus avec l'ajout d'une sécurité d'exception:
#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. }
Un exemple HSM assez simple: blinking_led
Ceci est un exemple standard de la distribution de SObjectizer. L'agent principal de cet exemple est un HSM qui peut être décrit par le graphique suivant:

Le code source de l'exemple:
#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; }
Minuteries, contrôle de surcharge pour un agent et répartiteur active_obj
Le contrôle de surcharge est l'un des principaux problèmes des acteurs: les files d'attente de messages pour les acteurs sont généralement illimitées, ce qui peut entraîner une croissance incontrôlée des files d'attente si un producteur de messages rapide envoie des messages plus rapidement que le destinataire peut les gérer. L'exemple suivant montre les fonctionnalités de SObjectizer comme limites de messages . Il permet de limiter le nombre de messages dans la file d'attente de l'agent et de défendre le destinataire des messages redondants.
Cet exemple montre également l'utilisation du temporisateur sous la forme d'un message périodique. La liaison des agents au répartiteur active_obj y est également indiquée. La liaison avec ce répartiteur signifie que chaque agent de la coopérative travaillera sur son propre thread de travail (par exemple, un agent devient un objet actif).
#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()); }); }); }
Si nous exécutons cet exemple, nous pouvons voir la sortie suivante:
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
Cette sortie montre que plusieurs messages qui ne peuvent pas entrer dans la limite définie sont rejetés et redirigés vers un autre récepteur.
Plus d'exemples
Un exemple plus ou moins similaire au code des applications réelles peut être trouvé dans notre projet de démonstration Shrimp . Un autre ensemble d'exemples intéressants peut être trouvé dans cette mini-série sur le "problème des philosophes de la restauration" classique: partie 1 et partie 2 . Et, bien sûr, il y a beaucoup d'exemples dans SObjectizer lui-même .
La réponse est très simple: elle est plus que suffisante pour nous. SObjectizer peut distribuer des millions de messages par seconde, et la vitesse réelle dépend des types de répartiteurs utilisés, des types de messages, du profil de charge, du matériel / OS / compilateur utilisé, etc. Dans une application réelle, nous n'utilisons généralement qu'une fraction de la vitesse de SObjectizer.
Les performances de SObjectizer pour votre tâche particulière dépendent fortement de votre tâche, de la solution particulière de cette tâche, de votre matériel ou de votre environnement virtuel, de la version de votre compilateur et de votre système d'exploitation. La meilleure façon de trouver une réponse à cette question est donc de créer votre propre benchmark qui sera spécifique à votre tâche et de l'expérimenter.
Si vous voulez des nombres de certains benchmarks synthétiques, il y a quelques programmes dans le dossier test / so_5 / bench de la distribution SObjectizer.
Nous pensons qu'un jeu de benchmarking comparant la vitesse de différents outils est un jeu de marketing. Nous avons fait une tentative dans le passé mais nous nous sommes vite rendu compte que ce n'était qu'une perte de temps. Nous ne jouons donc pas à ce jeu maintenant. Nous passons notre temps et nos ressources uniquement sur des benchmarks qui nous permettent de vérifier l'absence de dégradation des performances, de résoudre certains cas d'angle (comme les performances des mbox MPMC avec une grande quantité d'abonnés ou les performances d'un agent avec des centaines de milliers d'abonnements), pour accélérer certaines opérations spécifiques à SObjectizer (comme l'enregistrement / le désenregistrement d'une coopérative).
Nous laissons donc la comparaison de la vitesse à ceux qui aiment ce jeu et qui ont le temps de le jouer.
Pourquoi SObjectizer a exactement la même apparence?
Il existe plusieurs "frameworks d'acteurs" pour C ++, et tous ont un aspect différent. Il semble qu'il ait des raisons objectives: chaque cadre a ses caractéristiques uniques et cible des objectifs différents. De plus, les acteurs en C ++ peuvent être implémentés très différemment. Donc, la question principale n'est pas "pourquoi le framework X ne ressemble pas au framework Y?", Mais "pourquoi le framework X ressemble à ce qu'il est?"
Nous allons maintenant essayer de décrire brièvement les raisons des principales fonctionnalités de SObjectizer. Nous espérons que cela permettra une meilleure compréhension des capacités de SObjectizer. Mais avant de commencer, il est nécessaire de mentionner une chose très importante: SObjectizer n'a jamais été une expérience. Il a été créé pour résoudre le travail réel et a évolué en fonction de l'expérience de la vie réelle.
Les agents sont des objets de classes dérivées de agent_t
Les agents (ou acteurs) dans SObjectzer sont des objets de classes définies par l'utilisateur qui doivent être dérivées d'une classe spéciale agent_t
. Cela peut sembler redondant dans de minuscules exemples de jouets, mais notre expérience montre que cette approche simplifie considérablement le développement de logiciels réels où les agents ont généralement la taille sur plusieurs centaines de lignes (vous pouvez voir l'un des exemples ici , mais ce blog est en Russe). Parfois même en plusieurs milliers de lignes.
L'expérience nous montre qu'un simple agent avec la première version en cent lignes devient beaucoup plus gros et complexe au cours des prochaines années d'évolution. Ainsi, après cinq ans, vous pouvez trouver un monstre en mille lignes avec des dizaines de méthodes.
L'utilisation des classes nous permet de gérer la complexité des agents. Nous pouvons utiliser l'héritage des classes. Et nous pouvons également utiliser des classes de modèles. Ce sont des techniques très utiles qui simplifient considérablement le développement de familles d'agents ayant une logique similaire à l'intérieur.
Messages en tant qu'objets de structures / classes d'utilisateurs
Les messages dans SObjectizer sont des objets de structures ou de classes définies par l'utilisateur. Il y a au moins deux raisons à cela:
- le développement de SObjectizer-5 a commencé en 2010 alors que C ++ 11 n'était pas encore standardisé. Donc, au début, nous ne pouvions pas utiliser de telles fonctionnalités de C ++ 11 comme modèles variadic et classe
std::tuple
. Le seul choix que nous avions était l'utilisation d'un objet d'une classe héritée d'une classe spéciale message_t
. Désormais, il n'est plus nécessaire de dériver le type de message de message_t
, mais SObjectizer enveloppe de toute façon un objet utilisateur dans un objet dérivé de message_t
sous le capot; - le contenu d'un message peut facilement être modifié sans modification des signatures des gestionnaires d'événements. Et il y a un contrôle à partir d'un compilateur: si vous supprimez un champ d'un message ou changez son type, le compilateur vous informera d'un accès incorrect à ce champ.
L'utilisation de messages comme objets permet également de travailler avec des messages préalloués et de stocker un message reçu dans un conteneur et de le renvoyer plus tard.
Des coopératives d'agents
Une coopérative d'agents est probablement l'une des fonctionnalités uniques de SObjectizer. Une coopérative est un groupe d'agents qui doivent être ajoutés et supprimés de SObjectizer de manière transactionnelle. Cela signifie que si une coopérative contient trois agents, tous ces agents doivent être ajoutés à SObjectizer avec succès ou aucun d'entre eux ne doit être ajouté. De même, les trois agents doivent être supprimés de SObjectizer ou les trois agents doivent poursuivre leur travail.
Le besoin dans les coopératives a été découvert peu après le début de la vie de SObjectizer. Il est devenu évident que les agents seraient créés par des groupes et non par des instances uniques. Coops a été inventé pour simplifier la vie d'un développeur: il n'est pas nécessaire de contrôler la création du prochain agent et de supprimer les agents précédemment créés si la création d'un nouvel agent échoue.
Une coopérative peut également être considérée comme un superviseur en mode tout-en-un: si un agent de la coopérative tombe en panne, la coopérative entière sera supprimée de SObjectizer Environment et détruite (un utilisateur peut réagir à cela et recréer la coopérative à nouveau).
Boîtes de messages
Les boîtes de message sont une autre fonctionnalité unique de SObjectizer. Les messages dans SObjectizer sont envoyés à une boîte de message (mbox), pas directement à un agent. Il peut y avoir un récepteur derrière la mbox, ou il peut y avoir un million d'abonnés, ou il n'y a personne.
Mboxes nous permet de prendre en charge les fonctionnalités de base du modèle Publish-Subscribe. Une mbox peut être considérée comme un courtier MQ et le type de message peut être considéré comme un sujet.
Mboxes nous permet également de mettre en œuvre diverses formes intéressantes de livraison de messages. Par exemple, il existe une mbox à tour de rôle qui répartit les messages entre les abonnés de manière à tour de rôle. Il existe également une mbox conservée qui contient le dernier message envoyé et le renvoie automatiquement à chaque nouvel abonné. Il existe également un simple wrapper autour de libmosquitto qui permet d'utiliser MQTT comme transport pour une application distribuée.
Agents comme HSM
Les agents dans SObjectizer sont des machines à états. C'était dès le début simplement parce que SObjectizer a des racines dans le domaine SCADA, où les machines à états sont activement utilisées. Mais il est rapidement devenu évident que les agents sous la forme d'une machine d'état peuvent être utiles même dans des niches différentes (comme les applications de télécommunications et de finance).
La prise en charge des machines à états hiérarchiques (par exemple, les gestionnaires on_enter / on_exit, les états imbriqués, les limites de temps, etc.) a été ajoutée après un certain temps d'utilisation de SObjectizer en production. Et cette fonctionnalité a fait de SObjectizer un outil encore plus puissant et pratique.
Utilisation d'exceptions C ++
Les exceptions C ++ sont utilisées dans SObjectizer comme principal mécanisme de rapport d'erreurs. Malgré le fait que l'utilisation de l'exception C ++ peut parfois être coûteuse, nous avons décidé d'utiliser des exceptions au lieu de codes d'erreur.
Nous avons eu une expérience négative avec les codes d'erreur dans SObjectizer-4, où les exceptions n'étaient pas utilisées. Cela a conduit à l'ignorance des erreurs dans le code d'application et parfois des actions importantes n'ont pas été effectuées en raison d'une erreur lors de la création d'une nouvelle coopérative ou de l'envoi d'un message. Mais cette erreur a été ignorée et ce fait a été découvert beaucoup plus tard.
L'utilisation d'exceptions C ++ dans SObjectizer-5 permet d'écrire du code plus correct et plus robuste. Dans des cas habituels, les exceptions sont levées très rarement par SObjectizer, de sorte que l'utilisation d'exceptions n'a aucun impact négatif sur les performances de SObjectizer ou les performances des applications écrites par-dessus SObjectizer.
Pas de support pour les applications distribuées "out of box"
SObjectzer-5 n'a pas de support intégré pour les applications distribuées. Cela signifie que SObjectizer distribue des messages juste à l'intérieur d'un processus. Si vous devez organiser la distribution de messages inter-processus ou inter-notes, vous devez intégrer une sorte de IPC dans votre application.
Ce n'est pas parce que nous ne pouvons pas implémenter une forme d'IPC dans SObjectizer. Nous l'avions déjà dans SObjectizer-4. Et parce que nous avons une telle expérience, nous avons décidé de ne pas le faire dans SObjectizer-5. Nous avons appris qu'il n'y a pas un seul type d'IPC qui s'adapte parfaitement à différentes conditions.
Si vous souhaitez avoir une bonne communication inter-nœuds dans votre application, vous devez sélectionner les protocoles sous-jacents appropriés. Par exemple, si vous devez diffuser des millions de petits paquets avec des données de courte durée (comme la distribution de la mesure des conditions météorologiques actuelles), vous devez utiliser un IPC. Mais si vous devez transférer d'énormes BLOB (comme des flux vidéo 4K / 8K ou des archives contenant des données financières à l'intérieur), vous devez utiliser un autre type IPC.
Et nous ne parlons pas d'interopérabilité avec des logiciels écrits dans différentes langues ...
Vous pouvez croire qu'un certain «cadre d'acteur» universel peut vous fournir un IPC qui conviendra à différentes conditions. Mais nous savons que ce ne sont que des conneries de marketing. Notre expérience nous montre qu'il est beaucoup plus simple et beaucoup plus sûr d'ajouter l'IPC dont vous avez besoin dans votre application, puis de s'appuyer sur les idées, les besoins et les connaissances des auteurs d'un "framework d'acteur" tiers.
SObjectizer permet d'incorporer différents types d'IPC sous la forme de mbox personnalisées. Cela permet donc de cacher le fait de la distribution des messages sur un réseau aux utilisateurs d'un SObjectizer.
Au lieu de la conclusion
Le framework SObjectizer n'est pas un grand, mais ce n'est pas un petit. Il est donc impossible de donner au lecteur une impression assez profonde sur SObjectizer dans un seul aperçu. Pour cette raison, nous vous invitons à jeter un œil au projet SObjectizer.
SObjectizer lui-même vit sur GitHub . Il y a le Wiki du projet sur GitHub et nous vous recommandons de commencer à partir de SObjectizer 5.6 Basics , puis d'aller aux articles de la série approfondie. Pour ceux qui veulent aller plus loin, nous pouvons recommander Regardons sous la section hotte de SObjectizer .
Si vous avez des questions, vous pouvez nous les poser dans le groupe SObjectizer sur les groupes Google.