Solo eche un vistazo a SObjectizer si desea usar Actores o CSP en su proyecto C ++


Algunas palabras sobre SObjectizer y su historia


SObjectizer es un marco C ++ bastante pequeño que simplifica el desarrollo de aplicaciones multiproceso. SObjectizer permite a un desarrollador utilizar enfoques de modelos de Actor, Publicar-Suscribirse y Procesos secuenciales de comunicación (CSP). Es un proyecto OpenSource que se distribuye bajo licencia BSD-3-CLAUSE.


SObjectizer tiene una larga historia. SObjectizer en sí nació en 2002 como proyecto SObjectizer-4. Pero se basó en ideas del anterior SCADA Objectizer que se desarrolló entre 1995 y 2000. SObjectizer-4 fue de código abierto en 2006, pero su evolución se detuvo poco después. Una nueva versión de SObjectizer con el nombre SObjectizer-5 se inició en 2010 y fue de código abierto en 2013. La evolución de SObjectizer-5 todavía está en progreso y SObjectizer-5 ha incorporado muchas características nuevas desde 2013.


SObjectizer es más o menos conocido en el segmento ruso de Internet, pero casi desconocido fuera del exUSSR. Se debe a que SObjectizer se utilizó principalmente para proyectos locales en países exUSSR y muchos artículos, presentaciones y charlas sobre SObjectizer están en ruso.


Un nicho para SObjectizer y herramientas similares


Multithreading se utiliza en computación paralela , así como en computación concurrente . Pero hay una gran diferencia entre la computación paralela y concurrente. Y, como consecuencia, hay herramientas dirigidas a la computación paralela, y hay herramientas para la computación concurrente, y son diferentes.


En términos generales, la computación paralela se trata de usar varios núcleos para reducir los tiempos de cálculo. Por ejemplo, transcodificar un archivo de video de un formato a otro puede llevar una hora en un núcleo de CPU, pero solo 15 minutos en cuatro núcleos de CPU. Herramientas como OpenMP, Intel TBB, HPX o cpp-taskflow están diseñadas para ser utilizadas en computación paralela. Y esas herramientas son compatibles con los enfoques de esa área, como la programación basada en tareas o el flujo de datos.


La computación concurrente consiste en tratar muchas tareas (probablemente diferentes) al mismo tiempo. El servidor de bases de datos o MQ-broker pueden ser buenos ejemplos: un servidor debe aceptar una conexión, leer y analizar datos de las conexiones aceptadas, manejar las solicitudes recibidas (realizar varias acciones para cada solicitud), enviar respuestas, etc. Estrictamente hablando, no hay necesidad de usar subprocesos múltiples en la informática concurrente: todas esas tareas se pueden realizar en un solo subproceso de trabajo. Pero el uso de subprocesos múltiples y varios núcleos de CPU puede hacer que su aplicación sea más eficiente, escalable y receptiva.


Enfoques como Actor Model o CSP están destinados a tratar con la computación concurrente. Buenos ejemplos de actores de uso en el área de computación concurrente son el proyecto InfineSQL y Yandex Message-Queue . Ambos proyectos usan actores dentro.


Entonces, las herramientas como SObjectizer, QP / C ++ o CAF, que son compatibles con Actor Model, son útiles para resolver tareas desde el área de computación concurrente. Significa que el uso de SObjectizer probablemente no le dará nada en tareas como la conversión de transmisiones de video. Pero puede obtener un resultado muy diferente implementando un intermediario de mensajes sobre SObjectizer.


Descargo de responsabilidad


El uso de modelos Actor o CSP puede brindarle enormes beneficios en algunas tareas, pero no hay garantías de que esos modelos sean apropiados para su problema particular. La charla sobre la aplicabilidad de los modelos Actor o CSP está más allá del alcance de ese artículo. Supongamos que el modelo Actor y / o CSP es aplicable para sus tareas y usted sabe cómo usarlas de manera eficiente.


¿Qué SObjectizer puede dar a un usuario?


Principios de nada compartido y disparar y olvidar recién salidos de la caja


El uso de actores supone la ausencia de datos compartidos. Cada actor posee sus datos y estos datos no son visibles para nadie más. Este es un principio de nada compartido que es bien conocido en el desarrollo de aplicaciones distribuidas, por ejemplo. En la aplicación multiproceso, el principio de nada compartido tiene un beneficio importante: permite evitar problemas tan peligrosos para trabajar con datos compartidos como puntos muertos y carreras de datos.


La interacción entre actores (agentes) en SObjectizer se realiza solo a través de mensajes asincrónicos. Un agente envía un mensaje a otro agente y esta operación no bloquea al remitente (en un caso común).


La interacción asincrónica permite usar otro principio útil: disparar y olvidar . Cuando un agente necesita realizar alguna operación, envía (dispara) un mensaje y continúa su trabajo. En la mayoría de los casos, el mensaje será recibido y procesado.


Por ejemplo, puede haber un agente que lea las conexiones aceptadas y analice los datos entrantes. Si toda la PDU se lee y analiza, el agente solo envía esa PDU a otro agente-procesador y vuelve a leer / analizar nuevos datos entrantes.


Despachadores


Los despachadores son uno de los pilares de SObjectizer. Los despachadores proporcionan un contexto de trabajo (también conocido como hilo de trabajo) en el que un agente manejará los mensajes entrantes. En lugar de crear subprocesos de trabajo (o grupos de subprocesos) manualmente, un usuario crea despachadores y les vincula agentes. Un usuario puede crear tantos despachadores en una aplicación como desee.


Lo mejor con los despachadores y los agentes en SObjectizer es la separación de conceptos: los despachadores son responsables de administrar el contexto de trabajo y las propias colas de mensajes, los agentes realizan la lógica de la aplicación y no se preocupan por el contexto del trabajador. Permite mover un agente de un despachador a otro literalmente con un clic. Ayer, un agente trabajó en one_thread dispatcher, hoy podemos volver a vincularlo a active_obj dispatcher, y mañana podemos volver a vincularlo a thread_pool dispatcher. Sin cambiar una línea en la implementación del agente.


Hay ocho tipos de despachadores en SObjectizer-5.6.0 (y se puede encontrar otro en el proyecto complementario so5extra): desde los muy simples (one_thread o thread_pool) hasta los sofisticados (como adv_thread_pool o prio_dedicated_threads :: one_per_prio). Y un usuario puede escribir su propio despachador para condiciones específicas.


Las máquinas de estado jerárquicas son funciones integradas


Los agentes (actores) en SObjectizer son máquinas de estado: la reacción en un mensaje entrante depende del estado actual del agente. SObjectizer es compatible con la mayoría de las características de máquinas de estado jerárquico (HSM): estados anidados, historial profundo y superficial para un estado, controladores on_enter / on_exit, límites de tiempo para permanecer en un estado. Solo los estados ortogonales no son compatibles con SObjectizer ahora (no vimos la necesidad de esa característica en nuestros proyectos, y nadie nos pidió que agreguemos soporte para esa característica).


Canales similares a CSP recién listos


No es necesario usar los agentes de SObjectizer (también conocidos como actores). Toda la aplicación se puede desarrollar simplemente usando objetos std::thread y mchains de SObjectizer (también conocidos como canales CSP) . En ese caso, el desarrollo de aplicaciones con SObjectizer será algo similar al desarrollo en lenguaje Go (incluido un análogo de la construcción select de Go que permite esperar mensajes de varios canales).


Las cadenas de SObjectizer pueden tener una característica muy importante: mecanismo de contrapresión incorporado. Si un usuario crea un mchain de tamaño limitado y luego intenta insertar un mensaje en el mchain completo, la operación de envío puede bloquear al remitente durante algún tiempo. Permite resolver un problema famoso con un productor rápido y un consumidor lento.


Los mchains de SObjectizer tienen otra característica interesante: un mchain se puede usar como una herramienta de distribución de carga muy simple. Varios hilos pueden esperar recibir de la misma cadena al mismo tiempo. Si se envía un nuevo mensaje a ese mchain, solo un hilo leerá y manejará ese mensaje.


Solo una parte de una aplicación puede usar SObjectizer


No es necesario usar SObjectizer en cada parte de una aplicación. Solo se puede desarrollar una parte de una aplicación utilizando SObjectizer. Entonces, si ya usa Qt o wxWidgets, o Boost.Asio como marco principal para su aplicación, es posible usar SObjectize en solo un submódulo de su aplicación.


Teníamos experiencia en el uso de SObjectizer para el desarrollo de bibliotecas que ocultan el uso de SObjectizer como detalle de implementación. La API pública de esas bibliotecas no expuso la presencia de SObjectizer en absoluto. SObjectizer estaba completamente bajo el control de una biblioteca: la biblioteca se inició y detuvo SObjectizer cuando era necesario. Esas bibliotecas se usaron en aplicaciones que desconocían por completo la presencia de SObjectizer.


Si SObjectizer se usa solo en una parte de una aplicación, existe una tarea de comunicación entre las partes SObjectizer y no SObjectizer de la aplicación. Esta tarea se resuelve fácilmente: los mensajes de una parte que no es SObjectizer a la parte SObjectizer se pueden enviar a través del mecanismo ordinario de SObjectizer de entrega de mensajes. Los mensajes en la dirección opuesta se pueden entregar a través de mchains.


Puede ejecutar varias instancias de SObjectizer al mismo tiempo


SObjectizer permite ejecutar varias instancias de SObjectizer (denominado SObjectizer Environment) en una aplicación al mismo tiempo. Cada entorno SObjectizer será independiente de otros entornos similares.


Esta característica es invaluable en situaciones en las que tiene que construir una aplicación a partir de varios módulos independientes. Algunos módulos pueden usar SObjectizer, otros no. Los módulos que requieren SObjectizer pueden ejecutar su copia de SObjectizer Environment y eso no influirá en otros módulos de la aplicación.


Los temporizadores son parte de SObjectizer


El soporte de temporizadores en forma de mensajes retrasados ​​y periódicos es otra de las piedras angulares de SObjectizer. SObjectizer tiene varias implementaciones de mecanismos de temporizador (timer_wheel, timer_heap y timer_list) y puede manejar decenas, cientos y miles de millones de temporizadores en una aplicación. Un usuario puede elegir el mecanismo de temporizador más apropiado para una aplicación. Además, un usuario puede proporcionar su propia implementación de timer_thread / timer_manager si ninguno de los estándares es apropiado para las condiciones del usuario.


SObjectizer tiene varios puntos de personalización y opciones de ajuste


SObjectizer permite la personalización de varios mecanismos importantes. Por ejemplo, un usuario puede seleccionar una de las implementaciones estándar de timer_thread (o timer_manager). O puede proporcionar su propia implementación. Un usuario puede seleccionar una implementación de objetos de bloqueo utilizados por las colas de mensajes en los despachadores de SObjectizer. O puede proporcionar su propia implementación.


Un usuario puede implementar su propio despachador. Un usuario puede implementar su propio cuadro de mensaje. Un usuario puede implementar su propio sobre de mensaje. Un usuario puede implementar su propio event_queue_hook. Y así sucesivamente.


¿Dónde se puede o no se puede utilizar SObjectizer?


Es mucho más fácil decir dónde SObjectizer no se puede usar por razones objetivas. Entonces, comenzamos la discusión enumerando tales áreas y luego daremos algunos ejemplos del uso de SObjectizer en el pasado (y no solo en el pasado).


¿Dónde no se puede utilizar SObjectizer?



Como se ha dicho anteriormente, los modelos Actor y CSP no son una buena opción para la informática de alto rendimiento y otras áreas de la informática paralela. Entonces, si tiene que hacer múltiples matrices o transcodificar transmisiones de video, entonces las herramientas como OpenMP, Intel TBB, cpp-taskflow, HPX o MPI serán más adecuadas.


Sistemas duros en tiempo real


A pesar de que SObjectizer tiene sus raíces en los sistemas SCADA, la implementación actual de SObjectizer (también conocido como SObjectizer-5) no se puede utilizar en sistemas de tiempo real difíciles. Se debe principalmente al uso de memoria dinámica en la implementación de SObjectizer: los mensajes son objetos asignados dinámicamente (sin embargo, SObjectizer puede usar objetos preasignados como mensajes), los despachadores usan memoria dinámica para las colas de mensajes, incluso los límites de tiempo para los estados del agente usan objetos asignados dinámicamente para realizar comprobaciones de tiempo.


Desafortunadamente, el término "tiempo real" se usa en exceso en el mundo moderno. A menudo se dice sobre servicios web en tiempo real, como "aplicación web en tiempo real" o "análisis web en tiempo real", etc. El término "en línea" o "en vivo" es más apropiado para tales aplicaciones que el término "en tiempo real", incluso en forma de "tiempo real suave". Por lo tanto, si hablamos de algo como "aplicación web en tiempo real", entonces SObjectizer puede usarse fácilmente en tales sistemas "en tiempo real".


Sistemas embebidos restringidos


SObjectizer se basa en la biblioteca estándar de C ++: std::thread se usa para la gestión de hilos, std::atomic , std::mutex , std::condition_variable se usan para la sincronización de datos, RTTI y dynamic_cast se usan en SOizeject (por ejemplo , std::type_index se usan para la identificación del tipo de mensaje), las excepciones de C ++ se usan para informar errores.


Significa que SObjectizer no se puede usar en entornos donde tales instalaciones de la biblioteca estándar no están disponibles. Por ejemplo, en el desarrollo de sistemas embebidos restringidos donde solo se puede usar una parte de C ++ y C ++ stdlib.


¿Dónde se utilizó SObjectizer en el pasado?


Ahora intentamos hablar brevemente sobre algunos casos de uso del uso de SObjectizer en el pasado (y no solo en el pasado). Desafortunadamente, no es información completa porque hay algunos problemas.


En primer lugar, no conocemos todos los usos de SObjectizer. SObjectizer es un software gratuito que puede usarse incluso en proyectos propietarios. Por lo tanto, algunas personas simplemente obtienen SObjectizer y lo usan sin proporcionarnos ningún comentario. A veces adquirimos información sobre el uso de SObjectizer (pero sin ningún detalle), a veces no sabemos nada.


El segundo problema es el permiso para compartir información sobre el uso de SObjectizer en un proyecto en particular. Hemos recibido ese permiso muy raramente, en la mayoría de los casos, los usuarios de SObjectizer no desean abrir los detalles de implementación de sus proyectos (a veces entendemos las razones, a veces no).


Nos disculpamos por el hecho de que la información proporcionada es muy escasa y no contiene ningún detalle. Sin embargo, hay algunos ejemplos de uso de SObjectizer:


  • Gateway de agregación SMS / USSD que maneja más de 500 millones de mensajes por mes;
  • parte del sistema que atiende pagos en línea a través de cajeros automáticos de uno de los mayores bancos rusos;
  • simulación de modelos de procesos económicos (como parte de la investigación de doctorado);
  • Sistema distribuido de adquisición de datos y analítico. Datos recopilados en puntos distribuidos en todo el mundo por los comandos del nodo central. MQTT se utilizó como transporte para el control y la distribución de datos adquiridos;
  • entorno de prueba para verificar el sistema de control en tiempo real para equipos ferroviarios;
  • Sistema de control automático para escenarios de teatro. Más detalles se pueden encontrar aquí ;
  • componentes de la plataforma de gestión de datos en un sistema de publicidad en línea.

Una muestra de SObjectizer


Veamos varios ejemplos simples para probar SObjectizer. Esos son ejemplos muy simples que, esperamos, no requieren explicaciones adicionales, excluyendo los comentarios en el código.


El tradicional ejemplo "Hello, World" en el estilo de Actor Model


El ejemplo más simple con un solo agente que reacciona al mensaje de hello y termina su trabajo:


 #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!"); }); } 

Otra versión de "Hello, World" con agentes y modelo de publicación / suscripción


El ejemplo más simple con varios agentes, todos reaccionan a la misma instancia de mensaje de 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 ejecutamos ese ejemplo, podemos recibir algo así:


 Alice: greeting received: Hello, World! Bob: greeting received: Hello, World! Mike: greeting received: Hello, World! 

Ejemplo de "Hola mundo" en estilo CSP


Veamos un ejemplo de SObjectizer sin ningún actor, solo std::thread y canales similares a CSP.


Versión muy simple


Esta es una versión muy simple que no es segura a excepción:


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

Versión más robusta, pero aún simple.


Esta es una versión modificada del ejemplo que se muestra arriba con la adición de seguridad de excepción:


 #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 ejemplo de HSM bastante simple: blinking_led


Este es un ejemplo estándar de la distribución de SObjectizer. El agente principal de este ejemplo es un HSM que puede describirse en el siguiente diagrama de estado:


diagrama de estado de parpadeo


El código fuente del ejemplo:


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

Temporizadores, control de sobrecarga para un agente y despachador active_obj


El control de sobrecarga es uno de los principales problemas para los actores: las colas de mensajes para los actores suelen ser ilimitadas y esto puede conducir a un crecimiento descontrolado de las colas si un productor de mensajes rápidos envía mensajes más rápido de lo que el receptor puede manejarlos. El siguiente ejemplo muestra la característica de SObjectizer como límites de mensajes . Permite limitar el recuento de mensajes en la cola del agente y defender al receptor de mensajes redundantes.


Este ejemplo también muestra el uso del temporizador en forma de mensaje periódico. El enlace de los agentes al despachador active_obj también se muestra allí. La vinculación a ese despachador significa que cada agente de la cooperativa trabajará en su propio subproceso de trabajo (por ejemplo, un agente se convierte en un objeto activo).


 #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 ejecutamos ese ejemplo, podemos ver el siguiente resultado:


 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 

Este resultado muestra que varios mensajes que no pueden encajar en el límite definido son rechazados y redirigidos a otro receptor.


Más ejemplos


Un ejemplo más o menos similar al código de las aplicaciones de la vida real se puede encontrar en nuestro proyecto de demostración de Shrimp . Se puede encontrar otro conjunto de ejemplos interesantes en esta mini serie sobre el clásico "problema de los filósofos gastronómicos": parte 1 y parte 2 . Y, por supuesto, hay muchos ejemplos en SObjectizer .


¿Qué pasa con el rendimiento?


Hay una respuesta muy simple: es más que suficiente para nosotros. SObjectizer puede distribuir millones de mensajes por segundo, y la velocidad real depende de los tipos de despachadores utilizados, tipos de mensajes, perfil de carga, hardware / SO / compilador utilizado, etc. En una aplicación real, usualmente usamos solo una fracción de la velocidad de SObjectizer.


El rendimiento de SObjectizer para su tarea particular depende en gran medida de su tarea, la solución particular de esa tarea, de su hardware o entorno virtual, de la versión de su compilador y su sistema operativo. Entonces, la mejor manera de encontrar una respuesta a esa pregunta es crear un punto de referencia propio que sea específico para su tarea y experimentar con ella.


Si desea números de algunos puntos de referencia sintéticos, entonces hay algunos programas en la carpeta test / so_5 / bench de distribución SObjectizer.


Una nota sobre la comparación con diferentes herramientas.


Creemos que un juego de evaluación comparativa que compara la velocidad de diferentes herramientas es un juego de marketing. Intentamos en el pasado, pero rápidamente nos dimos cuenta de que es solo una pérdida de tiempo. Entonces no jugamos ese juego ahora. Gastamos nuestro tiempo y nuestros recursos solo en puntos de referencia que nos permiten verificar la ausencia de degradación del rendimiento, para resolver algunos casos esquimales (como el rendimiento de mboxes de MPMC con gran cantidad de suscriptores o el rendimiento de un agente con cientos de miles de suscripciones), acelerar algunas operaciones específicas de SObjectizer (como el registro / desregistro de una cooperativa).


Así que dejamos la comparación de velocidad a aquellos a quienes les gusta ese juego y tienen tiempo para jugarlo.


¿Por qué SObjectizer se ve exactamente como es?


Hay varios "marcos de actores" para C ++, y todos se ven diferentes. Parece que tiene algunas razones objetivas: cada marco tiene sus características únicas y objetivos diferentes. Además, los actores en C ++ pueden implementarse de manera muy diferente. Entonces, la pregunta principal no es "¿por qué el marco X no se parece al marco Y?", Sino "¿por qué el marco X se ve como es?"


Ahora intentaremos describir algunas razones detrás de las características principales del SObjectizer brevemente. Esperamos que permita una mejor comprensión de las habilidades de SObjectizer. Pero antes de comenzar, es necesario mencionar una cosa muy importante: SObjectizer nunca ha sido un experimento. Fue creado para resolver el trabajo en la vida real y ha ido evolucionando en función de la experiencia de la vida real.


Los agentes son objetos de clases derivadas de agent_t


Los agentes (también conocidos como actores) en SObjectzer son objetos de clases definidas por el usuario que deben derivarse de una clase especial agent_t . Puede parecer redundante en pequeños ejemplos de juguetes, pero nuestra experiencia muestra que el enfoque simplifica enormemente el desarrollo de software real donde los agentes generalmente tienen el tamaño en varios cientos de líneas (puede ver uno de los ejemplos aquí , pero esta publicación de blog está en Ruso). A veces incluso en varios miles de líneas.


La experiencia nos muestra que un agente simple con la primera versión en cien líneas se vuelve mucho más gordo y complejo en varios años de evolución. Entonces, después de cinco años, puedes encontrar un monstruo en mil líneas con docenas de métodos.


El uso de clases nos permite gestionar la complejidad de los agentes. Podemos usar la herencia de clases. Y también podemos usar clases de plantilla. Estas son técnicas muy útiles que simplifican enormemente el desarrollo de familias de agentes con una lógica similar en su interior.


Mensajes como objetos de estructuras / clases de usuario


Los mensajes en SObjectizer son objetos de estructuras o clases definidas por el usuario. Hay al menos dos razones para eso:


  • El desarrollo de SObjectizer-5 comenzó en 2010 cuando C ++ 11 aún no estaba estandarizado. Entonces, al principio, no podíamos usar características de C ++ 11 como plantillas variadas y clase std::tuple . La única opción que teníamos era el uso de un objeto de una clase heredada de una clase especial message_t . Ahora no es necesario derivar el tipo de mensaje de message_t , pero SObjectizer envuelve un objeto de usuario en message_t -derived object de todos modos bajo el capó;
  • El contenido de un mensaje se puede cambiar fácilmente sin modificar las firmas de los controladores de eventos. Y hay un control desde un compilador: si elimina algún campo de un mensaje o cambia su tipo, el compilador le informará sobre el acceso incorrecto a ese campo.

El uso de mensajes como objetos también permite trabajar con mensajes preasignados y almacenar un mensaje recibido en algún contenedor y reenviarlo más tarde.


Cooperativas de agentes


Una cooperativa de agentes es probablemente una de las características únicas de SObjectizer. Una cooperativa es un grupo de agentes que deben agregarse y eliminarse de SObjectizer de manera transaccional. Significa que si una cooperativa contiene tres agentes, entonces todos esos agentes deben agregarse a SObjectizer con éxito o ninguno de ellos debe agregarse. Del mismo modo, los tres agentes deben eliminarse de SObjectizer o los tres agentes deben continuar su trabajo.


La necesidad en las cooperativas se descubrió poco después del comienzo de la vida de SObjectizer. Se hizo evidente que los agentes serían creados por grupos, no por instancias únicas. Las cooperativas se inventaron para simplificar la vida de un desarrollador: no es necesario controlar la creación del siguiente agente y eliminar agentes creados previamente si falla la creación de un nuevo agente.


Una cooperativa también se puede ver como un supervisor en el modo todo en uno: si un agente de la cooperativa falla, entonces se eliminará toda la cooperativa del entorno SObjectizer y se destruirá (un usuario puede reaccionar a eso y volver a crear la cooperativa).


Cuadros de mensaje


Los cuadros de mensaje son otra característica única de SObjectizer. Los mensajes en SObjectizer se envían a un cuadro de mensaje (mbox), no a un agente directamente. Puede haber un receptor detrás del mbox, o puede haber un millón de suscriptores, o no puede haber nadie.


Mboxes nos permite admitir la funcionalidad básica del modelo Publicar-Suscribir. Un mbox puede verse como MQ-broker y el tipo de mensaje puede verse como un tema.


Mboxes también nos permite implementar varias formas interesantes de entrega de mensajes. Por ejemplo, hay un mbox round-robin que difunde mensajes entre suscriptores de manera round-robin. También hay un mbox retenido que contiene el último mensaje enviado y lo reenvía automáticamente para cada nuevo suscriptor. También hay un contenedor simple alrededor de libmosquitto que permite usar MQTT como transporte para una aplicación distribuida.


Agentes como HSM


Los agentes en SObjectizer son máquinas de estado. Fue desde el principio simplemente porque SObjectizer tiene raíces en el campo SCADA, donde las máquinas de estado se usan activamente. Pero rápidamente se hizo evidente que los agentes en forma de máquina de estado pueden ser útiles incluso en diferentes nichos (como aplicaciones de telecomunicaciones y finanzas).


Se agregó compatibilidad con máquinas de estados jerárquicos (por ejemplo, controladores on_enter / on_exit, estados anidados, límites de tiempo, etc.) después de algún tiempo de usar SObjectizer en producción. Y esta característica convirtió a SObjectizer en una herramienta aún más potente y conveniente.


Uso de excepciones de C ++


Las excepciones de C ++ se utilizan en SObjectizer como el principal mecanismo de informe de errores. A pesar de que el uso de la excepción C ++ a veces puede ser costoso, decidimos usar excepciones en lugar de códigos de error.


Tuvimos una experiencia negativa con los códigos de error en SObjectizer-4, donde no se utilizaron excepciones. Esto condujo a la ignorancia de errores en el código de la aplicación y, a veces, no se realizaron acciones importantes porque hubo un error al crear una nueva cooperativa o al enviar un mensaje. Pero este error fue ignorado y ese hecho fue descubierto mucho más tarde.


El uso de excepciones de C ++ en SObjectizer-5 permite escribir código más correcto y robusto. En casos habituales, SObjectizer genera excepciones muy raramente, por lo que el uso de la excepción no tiene un impacto negativo en el rendimiento de SObjectizer o el rendimiento de las aplicaciones escritas sobre SObjectizer.


No hay soporte para aplicaciones distribuidas "fuera de la caja"


SObjectzer-5 no tiene soporte incorporado para aplicaciones distribuidas. Significa que SObjectizer distribuye mensajes solo dentro de un proceso. Si necesita organizar la distribución de mensajes entre procesos o entre notas, debe integrar algún tipo de IPC en su aplicación.


Esto no se debe a que no podemos implementar alguna forma de IPC en SObjectizer. Ya lo teníamos en SObjectizer-4. Y debido a que tenemos esa experiencia, decidimos no hacerlo en SObjectizer-5. Aprendimos que no hay un tipo de IPC que se ajuste perfectamente a diferentes condiciones.


Si desea tener una buena comunicación entre nodos en su aplicación, debe seleccionar los protocolos subyacentes apropiados. Por ejemplo, si tiene que distribuir millones de paquetes pequeños con algunos datos de corta duración (como la distribución de la medición de las condiciones climáticas actuales), entonces debe usar un IPC. Pero si tiene que transferir enormes BLOB (como transmisiones de video 4K / 8K o archivos con datos financieros dentro), entonces debe usar otro tipo de IPC.


Y no hablamos de introperabilidad con software escrito en diferentes idiomas ...


Puede creer que un "marco de actores" universal puede proporcionarle un IPC que será bueno para diferentes condiciones. Pero sabemos que es solo una mierda de marketing. Nuestra experiencia nos muestra que es mucho más simple y seguro agregar el IPC que necesita en su aplicación que confiar en las ideas, necesidades y conocimiento de los autores de un "marco de actores" de terceros.


SObjectizer permite incorporar varios tipos de IPC en forma de mboxes personalizados. Por lo tanto, permite ocultar el hecho de la distribución de mensajes a través de una red de los usuarios de SObjectizer.


En lugar de la conclusión


El marco SObjectizer no es grande, pero no es pequeño. Por lo tanto, es imposible dar al lector una impresión bastante profunda sobre SObjectizer en una sola descripción general. Por eso, te invitamos a echar un vistazo al proyecto SObjectizer.


SObjectizer sí mismo vive en GitHub . Existe el Wiki del proyecto en GitHub y recomendamos comenzar desde SObjectizer 5.6 Basics y luego ir a artículos de series en profundidad. Para aquellos que quieran profundizar, podemos recomendar Veamos debajo de la sección del capó de SObjectizer .


Si tiene alguna pregunta, puede preguntarnos en el grupo de SObjectizer en los grupos de Google.

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


All Articles