
Ein paar Worte zu SObjectizer und seiner Geschichte
SObjectizer ist ein eher kleines C ++ - Framework, das die Entwicklung von Multithread-Anwendungen vereinfacht. Mit SObjectizer kann ein Entwickler Ansätze aus CSP-Modellen (Actor, Publish-Subscribe und Communicating Sequential Processes) verwenden. Es ist ein OpenSource-Projekt, das unter der BSD-3-CLAUSE-Lizenz vertrieben wird.
SObjectizer hat eine lange Geschichte. SObjectizer selbst wurde 2002 als SObjectizer-4-Projekt geboren. Es basierte jedoch auf Ideen aus früheren SCADA Objectizer, die zwischen 1995 und 2000 entwickelt wurden. SObjectizer-4 wurde 2006 als Open-Source-Version bereitgestellt, aber seine Entwicklung wurde bald danach gestoppt. Eine neue Version von SObjectizer mit dem Namen SObjectizer-5 wurde 2010 gestartet und 2013 als Open-Source-Version bereitgestellt. Die Entwicklung von SObjectizer-5 ist noch im Gange und SObjectizer-5 enthält seit 2013 viele neue Funktionen.
SObjectizer ist im russischen Segment des Internets mehr oder weniger bekannt, aber außerhalb des exUSSR fast unbekannt. Dies liegt daran, dass der SObjectizer hauptsächlich für lokale Projekte in exUSSR-Ländern verwendet wurde und viele Artikel, Präsentationen und Vorträge über SObjectizer auf Russisch sind.
Multithreading wird sowohl beim Parallel-Computing als auch beim Concurrent-Computing verwendet . Es gibt jedoch einen großen Unterschied zwischen parallelem und gleichzeitigem Computing. Infolgedessen gibt es Tools für das parallele Computing und Tools für das gleichzeitige Computing, die sich unterscheiden.
Beim parallelen Rechnen geht es grob gesagt darum, mehrere Kerne zu verwenden, um die Berechnungszeiten zu verkürzen. Das Umcodieren einer Videodatei von einem Format in ein anderes kann beispielsweise auf einem CPU-Kern eine Stunde dauern, auf vier CPU-Kernen jedoch nur 15 Minuten. Tools wie OpenMP, Intel TBB, HPX oder cpp-taskflow sind für die Verwendung im Parallel Computing konzipiert. Und diese Tools unterstützen geeignete Ansätze für diesen Bereich, wie z. B. aufgabenbasierte oder Datenflussprogrammierung.
Beim Concurrent Computing geht es darum, viele (wahrscheinlich unterschiedliche) Aufgaben gleichzeitig zu erledigen. Datenbankserver oder MQ-Broker können gute Beispiele sein: Ein Server muss eine Verbindung akzeptieren, Daten von akzeptierten Verbindungen lesen und analysieren, empfangene Anforderungen verarbeiten (mehrere Aktionen für jede Anforderung ausführen), Antworten senden usw. Streng genommen muss beim gleichzeitigen Rechnen kein Multithreading verwendet werden: Alle diese Aufgaben können mit nur einem Worker-Thread ausgeführt werden. Die Verwendung von Multithreading und mehreren CPU-Kernen kann Ihre Anwendung jedoch leistungsfähiger, skalierbarer und reaktionsfähiger machen.
Ansätze wie Actor Model oder CSP sind für den Umgang mit Concurrent Computing vorgesehen. Gute Anwendungsbeispiele Akteure im Bereich Concurrent Computing sind das InfineSQL-Projekt und die Yandex Message-Queue . In beiden Projekten werden Schauspieler eingesetzt.
Daher sind die Tools wie SObjectizer, QP / C ++ oder CAF, die das Actor Model unterstützen, hilfreich bei der Lösung von Aufgaben aus dem Bereich Concurrent Computing. Dies bedeutet, dass die Verwendung von SObjectizer bei Aufgaben wie der Konvertierung von Videostreams wahrscheinlich nichts bringt. Sie können jedoch ein ganz anderes Ergebnis erzielen, wenn Sie einen Nachrichtenbroker zusätzlich zu SObjectizer implementieren.
Haftungsausschluss
Die Verwendung von Actor- oder CSP-Modellen kann Ihnen bei einigen Aufgaben enorme Vorteile bringen, es gibt jedoch keine Garantie dafür, dass diese Modelle für Ihr spezielles Problem geeignet sind. Der Vortrag über die Anwendbarkeit von Schauspieler- oder CSP-Modellen geht über den Rahmen dieses Artikels hinaus. Nehmen wir an, dass das Akteur- oder / und CSP-Modell für Ihre Aufgaben anwendbar ist und Sie wissen, wie Sie sie effizient einsetzen können.
Was kann SObjectizer einem Benutzer geben?
Shared-Nothing- und Fire-and-Forget-Prinzipien sind sofort einsatzbereit
Die Verwendung von Akteuren setzt das Fehlen gemeinsamer Daten voraus. Jeder Akteur besitzt seine Daten und diese Daten sind für niemanden sichtbar. Dies ist ein Shared-Nothing-Prinzip , das beispielsweise in der Entwicklung verteilter Anwendungen bekannt ist. In Multithread-Anwendungen hat das Shared-Nothing-Prinzip einen wichtigen Vorteil: Es ermöglicht die Vermeidung gefährlicher Probleme bei der Arbeit mit gemeinsam genutzten Daten wie Deadlocks und Datenrennen.
Die Interaktion zwischen Akteuren (Agenten) in SObjectizer erfolgt nur über asynchrone Nachrichten. Ein Agent sendet eine Nachricht an einen anderen Agenten, und dieser Vorgang blockiert den Absender nicht (in einem häufigen Fall).
Die asynchrone Interaktion ermöglicht die Verwendung eines anderen nützlichen Prinzips: Feuer und Vergessen . Wenn ein Agent eine Operation ausführen muss, sendet er eine Nachricht (löst sie aus) und setzt seine Arbeit fort. In den meisten Fällen wird die Nachricht empfangen und verarbeitet.
Beispielsweise kann es einen Agenten geben, der akzeptierte Verbindungen liest und eingehende Daten analysiert. Wenn die gesamte PDU gelesen und analysiert wird, sendet der Agent diese PDU einfach an einen anderen Agentenprozessor und kehrt zum Lesen / Parsen neuer eingehender Daten zurück.
Dispatcher
Dispatcher sind einer der Eckpfeiler von SObjectizer. Dispatcher stellen einen Arbeitskontext (auch als Arbeitsthread bezeichnet) bereit, in dem ein Agent eingehende Nachrichten verarbeitet. Anstatt Arbeitsthreads (oder Thread-Pools) manuell zu erstellen, erstellt ein Benutzer Dispatcher und bindet Agenten an diese. Ein Benutzer kann in einer Anwendung so viele Disponenten erstellen, wie er möchte.
Das Beste an Dispatchern und Agenten in SObjectizer ist die Trennung von Konzepten: Disponenten sind für die Verwaltung des Arbeitskontexts und der eigenen Nachrichtenwarteschlangen verantwortlich, Agenten führen die Anwendungslogik aus und kümmern sich nicht um den Arbeitskontext. Es ermöglicht das Verschieben eines Agenten von einem Dispatcher zu einem anderen buchstäblich durch einen Klick. Gestern hat ein Agent an one_thread dispatcher gearbeitet, heute können wir es wieder an active_obj dispatcher binden und morgen können wir es wieder an thread_pool dispatcher binden. Ohne eine Zeile in der Implementierung des Agenten zu ändern.
In SObjectizer-5.6.0 gibt es acht Arten von Dispatchern (und eine weitere in so5extra-Begleitprojekt): von sehr einfachen (one_thread oder thread_pool) bis zu anspruchsvollen (wie adv_thread_pool oder prio_dedicated_threads :: one_per_prio). Und ein Benutzer kann seinen eigenen Dispatcher für bestimmte Bedingungen schreiben.
Hierarchische Zustandsautomaten sind integrierte Funktionen
Agenten (Akteure) in SObjectizer sind Zustandsautomaten: Die Reaktion auf eine eingehende Nachricht hängt vom aktuellen Status des Agenten ab. SObjectizer unterstützt die meisten Funktionen der hierarchischen Zustandsautomaten (HSM): verschachtelte Zustände, Deep- und Flat-Verlauf für einen Zustand, Handler on_enter / on_exit, Zeitlimits für den Verbleib in einem Zustand. Nur orthogonale Zustände werden in SObjectizer derzeit nicht unterstützt (wir haben in unseren Projekten keine Notwendigkeit für diese Funktion gesehen, und niemand hat uns gebeten, Unterstützung für diese Funktion hinzuzufügen).
CSP-ähnliche Kanäle sind sofort einsatzbereit
Es ist nicht erforderlich, die Agenten von SObjectizer (auch als Schauspieler bezeichnet) zu verwenden. Die gesamte Anwendung kann nur mit std::thread
Objekten und SObjectizer- Ketten (auch bekannt als CSP-Kanäle) entwickelt werden . In diesem Fall ähnelt die Anwendungsentwicklung mit SObjectizer der Entwicklung in der Sprache Go (einschließlich eines Analogons des select
von Go, mit dem Nachrichten von mehreren Kanälen abgewartet werden können).
Die Ketten von SObjectizer können ein sehr wichtiges Merkmal haben: den eingebauten Gegendruckmechanismus. Wenn ein Benutzer eine größenbeschränkte Kette erstellt und dann versucht, eine Nachricht in die vollständige Kette zu verschieben, kann der Sendevorgang den Absender für einige Zeit blockieren. Es ermöglicht die Lösung eines bekannten Problems mit einem schnellen Produzenten und einem langsamen Konsumenten.
Die Ketten von SObjectizer haben eine weitere interessante Funktion: Eine Kette kann als sehr einfaches Lastverteilungswerkzeug verwendet werden. Mehrere Threads können gleichzeitig auf den Empfang von derselben Mchain warten. Wenn eine neue Nachricht an diese Kette gesendet wird, liest und verarbeitet nur ein Thread diese Nachricht.
Nur ein Teil einer Anwendung kann SObjectizer verwenden
Es ist nicht erforderlich, SObjectizer in jedem Teil einer Anwendung zu verwenden. Mit SObjectizer kann nur ein Teil einer Anwendung entwickelt werden. Wenn Sie also bereits Qt oder wxWidgets oder Boost.Asio als Hauptframework für Ihre Anwendung verwenden, können Sie SObjectize in nur einem Submodul Ihrer App verwenden.
Wir hatten Erfahrung mit der Verwendung von SObjectizer für die Entwicklung von Bibliotheken, die die Verwendung von SObjectizer als Implementierungsdetail verbergen. Die öffentliche API dieser Bibliotheken hat das Vorhandensein von SObjectizer überhaupt nicht verfügbar gemacht. SObjectizer stand vollständig unter der Kontrolle einer Bibliothek: Die Bibliothek startete und stoppte SObjectizer nach Bedarf. Diese Bibliotheken wurden in Anwendungen verwendet, denen das Vorhandensein von SObjectizer überhaupt nicht bekannt war.
Wenn SObjectizer nur in einem Teil einer Anwendung verwendet wird, besteht eine Kommunikationsaufgabe zwischen SObjectizer- und Nicht-SObjectizer-Teilen der Anwendung. Diese Aufgabe ist leicht zu lösen: Nachrichten von einem Nicht-SObjectizer-Teil an einen SObjectizer-Teil können über den normalen SObjectizer-Mechanismus für die Nachrichtenübermittlung gesendet werden. Nachrichten in die entgegengesetzte Richtung können über Ketten übermittelt werden.
Sie können mehrere Instanzen von SObjectizer gleichzeitig ausführen
Mit SObjectizer können mehrere Instanzen von SObjectizer (als SObjectizer-Umgebung bezeichnet) gleichzeitig in einer Anwendung ausgeführt werden. Jede SObjectizer-Umgebung ist unabhängig von anderen solchen Umgebungen.
Diese Funktion ist in Situationen von unschätzbarem Wert, in denen Sie eine Anwendung aus mehreren unabhängigen Modulen erstellen müssen. Einige Module können SObjectizer verwenden, andere nicht. Die Module, für die SObjectizer erforderlich ist, können die Kopie der SObjectizer-Umgebung ausführen und haben keinen Einfluss auf andere Module in der Anwendung.
Timer sind Teil von SObjectizer
Die Unterstützung von Timern in Form von verzögerten und periodischen Nachrichten ist ein weiterer Eckpfeiler von SObjectizer. SObjectizer verfügt über mehrere Implementierungen von Timer-Mechanismen (timer_wheel, timer_heap und timer_list) und kann Zehntausende, Hunderte und Tausende von Millionen von Timern in einer Anwendung verarbeiten. Ein Benutzer kann den am besten geeigneten Zeitgebermechanismus für eine Anwendung auswählen. Darüber hinaus kann ein Benutzer seine eigene Implementierung von timer_thread / timer_manager bereitstellen, wenn keine der Standardimplementierungen für die Bedingungen des Benutzers geeignet ist.
SObjectizer verfügt über verschiedene Anpassungspunkte und Optimierungsoptionen
SObjectizer ermöglicht die Anpassung mehrerer wichtiger Mechanismen. Beispielsweise kann ein Benutzer eine der Standardimplementierungen von timer_thread (oder timer_manager) auswählen. Oder kann eine eigene Implementierung bereitstellen. Ein Benutzer kann eine Implementierung von Sperrobjekten auswählen, die von Nachrichtenwarteschlangen in den Dispatchern von SObjectizer verwendet werden. Oder kann eine eigene Implementierung bereitstellen.
Ein Benutzer kann seinen eigenen Dispatcher implementieren. Ein Benutzer kann ein eigenes Meldungsfeld implementieren. Ein Benutzer kann seinen eigenen Nachrichtenumschlag implementieren. Ein Benutzer kann seinen eigenen event_queue_hook implementieren. Und so weiter.
Wo kann SObjectizer eingesetzt werden oder nicht?
Es ist viel einfacher zu sagen, wo SObjectizer aus objektiven Gründen nicht verwendet werden kann. Wir beginnen die Diskussion mit der Aufzählung solcher Bereiche und geben dann einige Beispiele für die Verwendung von SObjectizer in der Vergangenheit (und nicht nur in der Vergangenheit).
Wo kann SObjectizer nicht verwendet werden?
Wie oben erwähnt, sind Schauspieler- und CSP-Modelle keine gute Wahl für Hochleistungsrechnen und andere Bereiche des Parallelrechnens. Wenn Sie also mehrere Matrizen verwenden oder Videostreams transkodieren müssen, sind Tools wie OpenMP, Intel TBB, cpp-taskflow, HPX oder MPI besser geeignet.
Harte Echtzeitsysteme
Trotz der Tatsache, dass SObjectizer seine Wurzeln in SCADA-Systemen hat, kann die aktuelle Implementierung von SObjectizer (auch bekannt als SObjectizer-5) nicht in harten Echtzeitsystemen verwendet werden. Dies liegt hauptsächlich an der Verwendung des dynamischen Speichers in der SObjectizer-Implementierung: Nachrichten sind dynamisch zugewiesene Objekte (SObjectizer kann jedoch vorab zugewiesene Objekte als Nachrichten verwenden), Disponenten verwenden dynamischen Speicher für Nachrichtenwarteschlangen, selbst Zeitlimits für Agentenstatus verwenden dynamisch zugewiesene Objekte Zeitprüfung durchführen.
Leider wird der Begriff "Echtzeit" in der modernen Welt stark überstrapaziert. Es wird oft über Echtzeit-Webdienste wie "Echtzeit-Webanwendung" oder "Echtzeit-Webanalyse" usw. gesprochen. Der Begriff "Online" oder "Live" ist für solche Anwendungen geeigneter als der Begriff "Echtzeit", selbst in "weicher Echtzeit" -Form. Wenn wir also von einer "Echtzeit-Webanwendung" sprechen, kann SObjectizer problemlos in solchen "Echtzeit" -Systemen verwendet werden.
Eingeschränkte eingebettete Systeme
SObjectizer basiert auf der C ++ - Standardbibliothek: std::thread
wird für die Threadverwaltung verwendet, std::atomic
, std::mutex
, std::condition_variable
werden für die Datensynchronisation verwendet, RTTI und dynamic_cast
werden verwendet, um SObjectizer zu vergrößern (zum Beispiel) , std::type_index
werden zur Identifizierung des Nachrichtentyps verwendet), C ++ - Ausnahmen werden zur Fehlerberichterstattung verwendet.
Dies bedeutet, dass SObjectizer nicht in Umgebungen verwendet werden kann, in denen solche Funktionen der Standardbibliothek nicht verfügbar sind. Zum Beispiel bei der Entwicklung von eingeschränkten eingebetteten Systemen, bei denen nur ein Teil von C ++ und C ++ stdlib verwendet werden kann.
Wo wurde in der Vergangenheit SObjectizer verwendet?
Jetzt versuchen wir kurz über einige Anwendungsfälle der Verwendung von SObjectizer in der Vergangenheit (und nicht nur in der Vergangenheit) zu sprechen. Leider sind die Informationen nicht vollständig, da einige Probleme auftreten.
Erstens kennen wir nicht alle Verwendungszwecke von SObjectizer. SObjectizer ist eine kostenlose Software, die auch in proprietären Projekten verwendet werden kann. Einige Leute bekommen einfach SObjectizer und verwenden es, ohne uns Feedback zu geben. Manchmal erhalten wir Informationen über die Verwendung von SObjectizer (jedoch ohne Details), manchmal wissen wir nichts.
Das zweite Problem ist die Erlaubnis, Informationen über die Verwendung von SObjectizer in einem bestimmten Projekt auszutauschen. Wir haben diese Erlaubnis sehr selten erhalten. In den meisten Fällen möchten Benutzer von SObjectizer keine Implementierungsdetails ihrer Projekte öffnen (manchmal verstehen wir die Gründe, manchmal nicht).
Wir entschuldigen uns dafür, dass die bereitgestellten Informationen so knapp aussehen und keine Details enthalten. Dennoch gibt es einige Beispiele für die Verwendung von SObjectizer:
- SMS / USSD-Aggregationsgateway, das mehr als 500 Millionen Nachrichten pro Monat verarbeitet;
- Teil des Systems für Online-Zahlungen über Geldautomaten einer der größten russischen Banken;
- Simulationsmodellierung wirtschaftlicher Prozesse (im Rahmen der Doktorarbeit);
- verteiltes Datenerfassungs- und Analysesystem. Daten, die an Punkten gesammelt wurden, die weltweit durch die Befehle des zentralen Knotens verteilt wurden. MQTT wurde als Transportmittel für die Kontrolle und die Verteilung erfasster Daten verwendet.
- Testumgebung zur Überprüfung des Echtzeitsteuerungssystems für Eisenbahnausrüstung;
- automatische Steuerung für Theaterkulisse. Weitere Details finden Sie hier ;
- Komponenten der Datenverwaltungsplattform in einem Online-Werbesystem.
Ein Vorgeschmack auf SObjectizer
Schauen wir uns einige einfache Beispiele an, um einen Eindruck von SObjectizer zu bekommen. Dies sind sehr einfache Beispiele, für die hoffentlich keine zusätzlichen Erklärungen erforderlich sind, mit Ausnahme der Kommentare im Code.
Das traditionelle "Hello, World" -Beispiel im Stil von Actor Model
Das einfachste Beispiel mit nur einem Agenten, der auf eine hello
Nachricht reagiert und seine Arbeit beendet:
#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!"); }); }
Eine andere Version von "Hello, World" mit Agenten und Publish / Subscribe-Modell
Das einfachste Beispiel mit mehreren Agenten, die alle auf dieselbe Instanz der hello
Nachricht reagieren:
#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!"); }); }
Wenn wir dieses Beispiel ausführen, können wir so etwas erhalten:
Alice: greeting received: Hello, World! Bob: greeting received: Hello, World! Mike: greeting received: Hello, World!
Beispiel "Hallo Welt" im CSP-Stil
Schauen wir uns ein Beispiel für SObjectizer ohne Akteure an, nur std::thread
und CSP-ähnliche Kanäle.
Sehr einfache Version
Dies ist eine sehr einfache Version, die nicht ausnahmesicher ist:
#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(); }
Robustere, aber dennoch einfache Version
Dies ist eine modifizierte Version des oben gezeigten Beispiels mit der zusätzlichen Ausnahmesicherheit:
#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. }
Ein ziemlich einfaches HSM-Beispiel: blinking_led
Dies ist ein Standardbeispiel aus der SObjectizer-Distribution. Der Hauptagent dieses Beispiels ist ein HSM, der durch das folgende Zustandsdiagramm beschrieben werden kann:

Der Quellcode des Beispiels:
#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; }
Timer, Überlastungskontrolle für einen Agenten und den Dispatcher active_obj
Die Überlastungskontrolle ist eines der Hauptprobleme für Akteure: Nachrichtenwarteschlangen für Akteure sind normalerweise unbegrenzt, und dies kann zu einem unkontrollierten Wachstum von Warteschlangen führen, wenn ein schneller Nachrichtenproduzent Nachrichten schneller sendet, als der Empfänger sie verarbeiten kann. Das folgende Beispiel zeigt die Funktion von SObjectizer als Nachrichtenlimits . Es ermöglicht die Begrenzung der Anzahl der Nachrichten in der Warteschlange des Agenten und den Schutz des Empfängers vor redundanten Nachrichten.
Dieses Beispiel zeigt auch die Verwendung des Timers in Form einer periodischen Nachricht. Dort wird auch die Bindung von Agenten an den Dispatcher active_obj angezeigt. Die Bindung an diesen Dispatcher bedeutet, dass jeder Agent des Coop an einem eigenen Worker-Thread arbeitet (z. B. wird ein Agent zu einem aktiven Objekt).
#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()); }); }); }
Wenn wir dieses Beispiel ausführen, sehen wir die folgende Ausgabe:
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
Diese Ausgabe zeigt, dass mehrere Nachrichten, die nicht in das definierte Limit passen, abgelehnt und an einen anderen Empfänger umgeleitet werden.
Weitere Beispiele
Ein Beispiel, das dem Code aus realen Anwendungen mehr oder weniger ähnlich ist, finden Sie in unserem Shrimp-Demo-Projekt . Eine weitere Reihe interessanter Beispiele finden Sie in dieser Mini-Serie über das klassische "Problem der Speisephilosophen": Teil 1 und Teil 2 . Und natürlich gibt es in SObjectizer selbst viele Beispiele .
Es gibt eine sehr einfache Antwort: Es ist mehr als gut genug für uns. SObjectizer kann Millionen von Nachrichten pro Sekunde verteilen. Die tatsächliche Geschwindigkeit hängt von den verwendeten Dispatcher-Typen, Nachrichtentypen, dem Ladeprofil, der verwendeten Hardware / dem verwendeten Betriebssystem / dem verwendeten Compiler usw. ab. In einer realen Anwendung verwenden wir normalerweise nur einen Bruchteil der SObjectizer-Geschwindigkeit.
Die Leistung von SObjectizer für Ihre bestimmte Aufgabe hängt stark von Ihrer Aufgabe, der speziellen Lösung dieser Aufgabe, Ihrer Hardware oder virtuellen Umgebung, der Version Ihres Compilers und Ihrem Betriebssystem ab. Der beste Weg, um eine Antwort auf diese Frage zu finden, besteht darin, einen eigenen Benchmark zu erstellen, der für Ihre Aufgabe spezifisch ist, und damit zu experimentieren.
Wenn Sie Zahlen von einigen synthetischen Benchmarks möchten, befinden sich einige Programme im Ordner test / so_5 / Bench der SObjectizer-Distribution.
Wir denken, dass ein Benchmarking-Spiel, das die Geschwindigkeit verschiedener Tools vergleicht, ein Marketing-Spiel ist. Wir haben in der Vergangenheit einen Versuch unternommen, aber schnell erkannt, dass dies nur eine Verschwendung unserer Zeit ist. Also spielen wir dieses Spiel jetzt nicht. Wir verwenden unsere Zeit und unsere Ressourcen nur für Benchmarks, mit denen wir das Fehlen von Leistungseinbußen überprüfen und einige Eckfälle beheben können (z. B. die Leistung von MPMC-Mboxes mit einer großen Anzahl von Abonnenten oder die Leistung eines Agenten mit Hunderttausenden von Abonnements). um einige SObjectizer-spezifische Vorgänge zu beschleunigen (wie die Registrierung / Abmeldung eines Coops).
Also überlassen wir den Geschwindigkeitsvergleich denen, die dieses Spiel mögen und Zeit haben, es zu spielen.
Warum sieht SObjectizer genauso aus wie es ist?
Es gibt mehrere "Actor Frameworks" für C ++, die alle unterschiedlich aussehen. Es scheint, dass es einige objektive Gründe hat: Jedes Framework hat seine einzigartigen Merkmale und zielt auf unterschiedliche Ziele ab. Darüber hinaus können Akteure in C ++ sehr unterschiedlich implementiert werden. Die Hauptfrage lautet also nicht "Warum sieht Framework X nicht wie Framework Y aus?", Sondern "Warum sieht Framework X so aus, wie es ist?"
Jetzt werden wir versuchen, einige Gründe für die Hauptfunktionen des SObjectizers kurz zu beschreiben. Wir hoffen, dass dies ein besseres Verständnis der Fähigkeiten von SObjectizer ermöglicht. Bevor wir beginnen, muss jedoch eines sehr wichtig erwähnt werden: SObjectizer war noch nie ein Experiment. Es wurde für die Lösung realer Arbeiten entwickelt und hat sich basierend auf den realen Erfahrungen weiterentwickelt.
Agenten sind Objekte von Klassen, die von agent_t abgeleitet sind
Agenten (auch als Akteure bezeichnet) in SObjectzer sind Objekte benutzerdefinierter Klassen, die von einer speziellen Klasse agent_t
abgeleitet werden agent_t
. In winzigen Spielzeugbeispielen mag es überflüssig aussehen, aber unsere Erfahrung zeigt, dass dieser Ansatz die Entwicklung realer Software erheblich vereinfacht, bei der Agenten normalerweise die Größe in mehreren hundert Zeilen haben (Sie können eines der Beispiele hier sehen , aber dieser Blog-Beitrag ist in Russisch). Manchmal sogar in mehreren tausend Zeilen.
Die Erfahrung zeigt uns, dass ein einfacher Agent mit der ersten Version in hundert Zeilen in einigen nächsten Jahren der Evolution viel dicker und komplexer wird. Nach fünf Jahren können Sie also ein Monster in tausend Zeilen mit Dutzenden von Methoden finden.
Durch die Verwendung von Klassen können wir die Komplexität von Agenten verwalten. Wir können die Vererbung von Klassen verwenden. Und wir können auch Vorlagenklassen verwenden. Dies sind sehr nützliche Techniken, die die Entwicklung von Agentenfamilien mit ähnlicher Logik erheblich vereinfachen.
Nachrichten als Objekte von Benutzerstrukturen / -klassen
Nachrichten in SObjectizer sind Objekte benutzerdefinierter Strukturen oder Klassen. Dafür gibt es mindestens zwei Gründe:
- Die Entwicklung von SObjectizer-5 begann 2010, als C ++ 11 noch nicht standardisiert war. Am Anfang konnten wir solche Funktionen von C ++ 11 nicht als variable Vorlagen und als
std::tuple
tuple-Klasse verwenden. Die einzige Wahl, die wir hatten, war die Verwendung eines Objekts einer Klasse, die von einer speziellen Klasse message_t
. Jetzt ist es nicht mehr erforderlich, den Nachrichtentyp von message_t
, aber SObjectizer verpackt ein Benutzerobjekt ohnehin unter der Haube in ein von message_t
abgeleitetes Objekt. - Der Inhalt einer Nachricht kann leicht geändert werden, ohne dass die Signaturen der Ereignishandler geändert werden müssen. Und es gibt ein Steuerelement von einem Compiler: Wenn Sie ein Feld aus einer Nachricht entfernen oder dessen Typ ändern, informiert Sie der Compiler über einen falschen Zugriff auf dieses Feld.
Die Verwendung von Nachrichten als Objekte ermöglicht es auch, mit vorab zugewiesenen Nachrichten zu arbeiten und eine empfangene Nachricht in einem Container zu speichern und später erneut zu senden.
Coops von Agenten
Eine Gruppe von Agenten ist wahrscheinlich eine der einzigartigen Funktionen von SObjectizer. Ein Coop ist eine Gruppe von Agenten, die SObjectizer auf transaktionale Weise hinzugefügt und daraus entfernt werden sollten. Wenn ein Coop drei Agenten enthält, sollten alle diese Agenten erfolgreich zu SObjectizer hinzugefügt werden, oder es sollte keiner von ihnen hinzugefügt werden. Ebenso sollten alle drei Agenten aus SObjectizer entfernt werden oder alle drei Agenten sollten ihre Arbeit fortsetzen.
Der Bedarf an Coops wurde kurz nach dem Beginn des SObjectizer-Lebens entdeckt. Es wurde klar, dass Agenten von Gruppen und nicht von einzelnen Instanzen erstellt werden. Coops wurden erfunden, um das Leben eines Entwicklers zu vereinfachen: Es ist nicht erforderlich, die Erstellung des nächsten Agenten zu steuern und zuvor erstellte Agenten zu entfernen, wenn die Erstellung eines neuen Agenten fehlschlägt.
Ein Coop kann auch als Supervisor im All-for-One-Modus angesehen werden: Wenn ein Agent aus dem Coop ausfällt, wird der gesamte Coop aus der SObjectizer-Umgebung entfernt und zerstört (ein Benutzer kann darauf reagieren und den Coop erneut erstellen).
Meldungsfelder
Nachrichtenfelder sind eine weitere einzigartige Funktion von SObjectizer. Nachrichten in SObjectizer werden an ein Nachrichtenfeld (mbox) und nicht direkt an einen Agenten gesendet. Es kann einen Empfänger hinter der mbox geben, oder es kann eine Million Abonnenten geben, oder es kann niemand geben.
Mit Mboxes können wir die grundlegenden Funktionen des Publish-Subscribe-Modells unterstützen. Eine mbox kann als MQ-Broker und der Nachrichtentyp als Thema angesehen werden.
Mit Mboxes können wir auch verschiedene interessante Formen der Nachrichtenübermittlung implementieren. Zum Beispiel gibt es eine Round-Robin-Mbox , die Nachrichten zwischen Round-Robin-Teilnehmern verteilt. Es gibt auch eine beibehaltene mbox , die die zuletzt gesendete Nachricht enthält und diese automatisch für jeden neuen Abonnenten erneut sendet. Es gibt auch einen einfachen Wrapper um libmosquitto , mit dem MQTT als Transport für eine verteilte Anwendung verwendet werden kann.
Agenten als HSM
Agenten in SObjectizer sind Zustandsautomaten. Dies war von Anfang an einfach deshalb so, weil SObjectizer Wurzeln im SCADA-Bereich hat, in dem Zustandsautomaten aktiv verwendet werden. Es wurde jedoch schnell klar, dass Agenten in Form einer Zustandsmaschine auch in verschiedenen Nischen (wie Telekommunikations- und Finanzanwendungen) nützlich sein können.
Die Unterstützung hierarchischer Zustandsautomaten (z. B. on_enter / on_exit-Handler, verschachtelte Zustände, Zeitlimits usw.) wurde nach einiger Zeit der Verwendung von SObjectizer in der Produktion hinzugefügt. Und diese Funktion machte SObjectizer noch leistungsfähiger und bequemer.
Verwendung von C ++ - Ausnahmen
C ++ - Ausnahmen werden in SObjectizer als Hauptmechanismus für die Fehlerberichterstattung verwendet. Trotz der Tatsache, dass die Verwendung von C ++ - Ausnahmen manchmal kostspielig sein kann, haben wir uns entschieden, Ausnahmen anstelle von Fehlercodes zu verwenden.
Wir hatten eine negative Erfahrung mit Fehlercodes in SObjectizer-4, wo keine Ausnahmen verwendet wurden. Dies führte dazu, dass Fehler im Anwendungscode nicht erkannt wurden und manchmal wichtige Aktionen nicht ausgeführt wurden, weil beim Erstellen eines neuen Coops oder beim Senden einer Nachricht ein Fehler aufgetreten ist. Dieser Fehler wurde jedoch ignoriert und viel später entdeckt.
Die Verwendung von C ++ - Ausnahmen in SObjectizer-5 ermöglicht das Schreiben von korrekterem und robusterem Code. In der Regel werden Ausnahmen von SObjectizer sehr selten ausgelöst, sodass die Verwendung von Ausnahmen keine negativen Auswirkungen auf die Leistung von SObjectizer oder die Leistung von Anwendungen hat, die über SObjectizer geschrieben wurden.
Keine Unterstützung für verteilte Anwendungen "out of box"
SObjectzer-5 bietet keine integrierte Unterstützung für verteilte Anwendungen. Dies bedeutet, dass SObjectizer Nachrichten nur innerhalb eines Prozesses verteilt. Wenn Sie die Verteilung von Nachrichten zwischen Prozessen oder Notizen organisieren müssen, müssen Sie eine Art IPC in Ihre Anwendung integrieren.
Dies liegt nicht daran, dass wir in SObjectizer keine IPC-Form implementieren können. Das hatten wir schon in SObjectizer-4. Und weil wir solche Erfahrungen haben, haben wir uns entschieden, dies in SObjectizer-5 nicht zu tun. Wir haben gelernt, dass es keinen IPC-Typ gibt, der perfekt für unterschiedliche Bedingungen geeignet ist.
Wenn Sie eine gute Kommunikation zwischen Knoten in Ihrer Anwendung wünschen, müssen Sie die entsprechenden zugrunde liegenden Protokolle auswählen. Wenn Sie beispielsweise Millionen kleiner Pakete mit kurzlebigen Daten verteilen müssen (z. B. Verteilung der Messwerte der aktuellen Wetterbedingungen), müssen Sie einen IPC verwenden. Wenn Sie jedoch große BLOBs (wie 4K / 8K-Videostreams oder Archive mit darin enthaltenen Finanzdaten) übertragen müssen, müssen Sie einen anderen IPC-Typ verwenden.
Und wir sprechen nicht über Introperabilität mit Software, die in verschiedenen Sprachen geschrieben ist ...
Sie können glauben, dass ein universelles "Akteur-Framework" Ihnen einen IPC bieten kann, der für verschiedene Bedingungen geeignet ist. Aber wir wissen, dass es nur Marketing-Bullshit ist. Unsere Erfahrung zeigt uns, dass es viel einfacher und sicherer ist, den IPC, den Sie in Ihrer Anwendung benötigen, hinzuzufügen, als sich auf Ideen, Bedürfnisse und Kenntnisse der Autoren eines "Actor Framework" einer dritten Partei zu verlassen.
Mit SObjectizer können verschiedene IPC-Typen in Form von benutzerdefinierten Mboxes integriert werden. Auf diese Weise kann die Tatsache der Nachrichtenverteilung über ein Netzwerk vor den Benutzern eines SObjectizers verborgen werden.
Anstelle der Schlussfolgerung
Das SObjectizer-Framework ist kein großes, aber kein kleines. Es ist daher unmöglich, dem Leser in nur einer Übersicht einen ziemlich tiefen Eindruck von SObjectizer zu vermitteln. Aus diesem Grund laden wir Sie ein, sich das SObjectizer-Projekt anzusehen.
SObjectizer selbst lebt auf GitHub . Es gibt das Projekt-Wiki auf GitHub und wir empfehlen, mit SObjectizer 5.6 Basics zu beginnen und dann zu Artikeln aus ausführlichen Serien zu gehen. Für diejenigen, die tiefer gehen möchten, empfehlen wir einen Blick unter den Haubenbereich des SObjectizers .
Wenn Sie Fragen haben, können Sie uns in der SObjectizer-Gruppe in den Google-Gruppen Fragen stellen.