The roots of the SObjectizer originate in the topic of automated process control systems (APCS). But we used SObjectizer in areas far from ASUTP. Therefore, sometimes there is nostalgia from the category “oh, I haven’t taken checkers for a long time ...” One day because of this, one of the most voluminous examples appeared in SObjectizer - machine_control . At that time, I really wanted to “shake the old days”, to simulate the task of controlling the equipment in modern SObjectizer. Well, under the guise of stuffing different tasty SObjectizer features like delivery filters, template agents and a dispatcher with priority support as an example. Today we will try to tell and show how this all works.
Photo by Mike Boening
Suppose that we are dealing with a machine or a machine at a factory that has an engine inside. Well, let's say a conveyor belt drive. Or is it a pump that pumps water? It does not matter. The important thing is that when the engine is running, it heats up. And when it heats up, it needs to be cooled. Therefore, a cooling fan is installed near the engine. This fan must be turned on if the engine warms up over 70 degrees. The fan should be turned off if the engine has cooled down to 50 degrees as a result of cooling. If, despite cooling, the engine continues to heat up and its temperature reaches 95 degrees, then the engine must be turned off and wait until it cools down to 50 degrees.
Naturally, we would like to see how all this happens in dynamics. We need to see if the engine is working now, what is the temperature of the engine, whether the cooling fan is on. To do this, in the implementation of machine_control, we use a simple periodic print of all this information on the console.
Well, in order to make it more interesting for us, we will do the work not with one engine, but with several. They all work the same way, but their properties are different. Some of them heat up faster, some slower. Therefore, if you observe the work of, say, four engines, it will seem that each of them lives its own life.
It is clear that any problem can be solved in several ways. The method described below is just one of the possible. He was chosen not so much because of considerations of simplicity and practicality, but because of the opportunity to demonstrate different features of SObjectizer. Therefore, it is better to treat the following text as a demonstration. Moreover, in production everything will be much more serious and terrible;)
There are several agents-machines, there is a special shared mailbox. The agent agents from time to time send messages about their current status to this shared mailbox.
From the general mailbox, messages about the status of the agent-machine come to two completely different agents. The first of these, total_status_dashboard, collects information about the statuses of agent machines and periodically displays information about what is happening on the standard output stream. Due to this, by running the example we see something like this:
The second agent is statuses_analyser. It receives status information in order to control what is happening with the machines and determine the moments when some of the machines need external influence. When impact is required, the statuses_analyser agent sends another message to the same shared mailbox. The machine_controller agents respond to this message. They already decide what kind of control action should be applied to a particular machine and send the corresponding message directly to the corresponding agent machine.
That's all. We will try to consider the rest of the moments in detail below.
Let's start the analysis of the implementation of our example “from the stove”, i.e. from the machine itself with the engine and cooling fan. In real life, we would have some kind of real hardware to which some sensors and controllers would be connected. And with all of this kitchen would have to work from the program through a particular interface, and maybe through several different interfaces.
But we have a fictional example, so we don’t need to poll any ports and read data from real equipment. We need something pretending to be a working machine. For this we have an agent a_machine_t who:
Despite the fact that this agent is engaged in imitation, in due time, it was possible to use similar logic in APCS tasks. The timer event agent accessed the equipment through some interface and collected information from the sensors. After that, he transformed the captured information into the desired form and sent it to those who could properly process this information.
Let's look at what this a_machine_t agent looks like, but first we introduce a few definitions that we will need in the implementation of a_machine_t:
// . enum class engine_state_t { on, off }; // . enum class cooler_state_t { on, off }; // - . struct turn_engine_on : public so_5::signal_t {}; struct turn_engine_off : public so_5::signal_t {}; // - . struct turn_cooler_on : public so_5::signal_t {}; struct turn_cooler_off : public so_5::signal_t {}; // . struct machine_status { // - . // , . const std::string m_id; // . const engine_state_t m_engine_status; // . const cooler_state_t m_cooler_status; // . const float m_engine_temperature; };
Accordingly, our agent a_machine_t will receive control commands in the form of turn_engine_on / turn_engine_off message signals and turn_cooler_on / turn_cooler_off, and will notify of its status by sending a machine_status message.
Now you can proceed to the consideration of the agent a_machine_t. Let's start with the giblets:
class a_machine_t : public so_5::agent_t { // // . struct update_status : public so_5::signal_t {}; // - : // , , const state_t st_engine_on{ this, "on" }; // , . const state_t st_engine_off{ this, "off" }; // - . const std::string m_id; // machine_status. const so_5::mbox_t m_status_distrib_mbox; // : // , const float m_initial_temperature; // , const float m_engine_heating_step; // . const float m_cooler_impact_step; // . float m_engine_temperature; // . engine_state_t m_engine_status = engine_state_t::off; cooler_state_t m_cooler_status = cooler_state_t::off; // ID update_status. // SO-5 ID // , // . so_5::timer_id_t m_update_status_timer;
The a_machine_t agent is a very simple state machine with two states: “engine on” and “engine off”. In each of them, he responds to some messages differently. In order to represent the agent as a finite state machine in a SObjectizer, we needed two separate attributes, st_engine_on and st_engine_off.
At its start, the agent initiates a periodic update_status message. Each time he receives this message, the agent recalculates the value of m_engine_temperature according to whether the engine and the fan are working or not. After that, the machine_status message with the current readings is sent to the m_status_distrib_mbox mailbox.
The agent's constructor looks bulky due to the abundance of initial parameters, but in fact it is trivial, so we will not consider it.
But further more interesting. First, it is the special method so_define_agent (), which is used so that the agent can configure itself to work inside the SObjectizer. Our a_machine_t needs to go to its initial state and subscribe to the messages it needs. Here's what it looks like:
virtual void so_define_agent() override { this >>= st_engine_off; st_engine_on .event< turn_engine_off >( &a_machine_t::evt_turn_engine_off ) .event< turn_cooler_on >( &a_machine_t::evt_turn_cooler_on ) .event< turn_cooler_off >( &a_machine_t::evt_turn_cooler_off ) .event< update_status >( &a_machine_t::evt_update_status_when_engine_on ); st_engine_off .event< turn_engine_on >( &a_machine_t::evt_turn_engine_on ) .event< turn_cooler_on >( &a_machine_t::evt_turn_cooler_on ) .event< turn_cooler_off >( &a_machine_t::evt_turn_cooler_off ) .event< update_status >( &a_machine_t::evt_update_status_when_engine_off ); }
It can be noted that the agent in different states responds to the update_status signal with different handlers. You can also see that in the st_engine_on state, the turn_engine_on signal is ignored, since there is no point in switching on an already running engine. Similarly, turn_engine_off is in st_engine_off state.
Regarding the so_define_agent () method, it is impossible not to make a small lyrical digression: at first glance, this method looks redundant and it seems that one could do without it. After all, you can perform all agent settings directly in the constructor.
Indeed, it is possible. In the simplest cases, this is done. But if inheritance is used for agents, then setting an agent in so_define_agent () is more convenient than in the constructor. Classes inheritors receive a simple and convenient way to interfere with the base class settings (for example, you can simply not call so_define_agent () for the base class, then all settings will be made by the derived class).
In addition, in our experience, a separate method so_define_agent () begins to justify itself in large projects written by different people: you open someone else's code, you immediately look at someone else's so_define_agent () and see everything in one place. Greatly saves time and effort.
The next important method is so_evt_start (). The SObjectizer automatically calls it on all agents that start working inside the SObjectizer. Our a_machine_t uses so_evt_start () to start sending a periodic update_status message:
virtual void so_evt_start() override { // update_status // ID , , // . m_update_status_timer = so_5::send_periodic< update_status >( // . *this, // . std::chrono::milliseconds(0), // 200ms. std::chrono::milliseconds(200) ); }
Next come the a_machine_t agent event handlers. An event handler is a method that SObjectizer will call when the agent receives the corresponding incident message. The correspondence between the incident message and the handler is set when subscribing to the message. So, subscription of the form:
st_engine_on .event< turn_engine_off >( &a_machine_t::evt_turn_engine_off )
tells SObjectizer that when an agent receives a message like turn_engine_off, then the agent should call the method evt_turn_engine_off ().
The a_machine_t agent has four simple handlers whose work does not make sense to explain:
void evt_turn_engine_off() { // . this >>= st_engine_off; // . m_engine_status = engine_state_t::off; } void evt_turn_engine_on() { this >>= st_engine_on; m_engine_status = engine_state_t::on; } void evt_turn_cooler_off() { // . // . m_cooler_status = cooler_state_t::off; } void evt_turn_cooler_on() { m_cooler_status = cooler_state_t::on; }
But on the reactions to the periodic update_status message, you will need to give a few explanations. First look at the reaction to update_status when the engine is running:
void evt_update_status_when_engine_on() { m_engine_temperature += m_engine_heating_step; if( cooler_state_t::on == m_cooler_status ) m_engine_temperature -= m_cooler_impact_step; distribute_status(); }
Since the engine is working for us, and therefore it is heating up, we must increase the current temperature. However, if the cooling fan is on, the temperature needs to be adjusted to the effect of cooling.
When the update_status signal is processed to the st_engine_off state, i.e. when the engine is off, we only need to consider the effect of cooling, if it is now on:
void evt_update_status_when_engine_off() { if( cooler_state_t::on == m_cooler_status ) { m_engine_temperature -= m_cooler_impact_step; // // . if( m_engine_temperature < m_initial_temperature ) m_engine_temperature = m_initial_temperature; } distribute_status(); }
Well, the auxiliary method distribute_status () has a completely trivial implementation, since its only task is to send the machine_status message to the mailbox specifically designed for this:
void distribute_status() { // send- machine_status, // send. // // m_status_distrib_mbox. so_5::send< machine_status >( // . m_status_distrib_mbox, // // machine_status. m_id, m_engine_status, m_cooler_status, m_engine_temperature ); }
Perhaps someone from the readers had a question: why does a_machine_t not decide on switching the fan and the engine on / off? Why, instead of independently analyzing what is happening and taking appropriate actions, does a_machine_t only periodically send somewhere messages about its current state?
This is because decomposition :)
When we design a system based on an object-oriented approach, we strive to ensure that each object is responsible for its task and the effect we need would be achieved by combining them. Similarly, the same happens when designing a system based on actors (agents): let each actor be responsible for his task, and we will form a solution by combining them.
Therefore, the a_machine_t agent solves only one task: implements the interface with the equipment (in our example, it simulates this interface). In a real task, a_machine_t would be engaged in setting / resetting bitics, reading and writing bytes to some communication port with checks for successful I / O operations, monitoring data transfer speeds, timeouts, and so on. Low-level things.
So the task of a_machine_t is to get meaningful information from the device, suitable for further processing, to give this information to someone up, to take the command for the device from above, to convert this command into a sequence of actions understood by the device. What a_machine_t to the best of the limitations of a specific imitation does.
The a_total_status_dashboard_t agent should collect machine_status messages, aggregate the information from these messages, and periodically map the aggregated information to the standard output stream.
To do its job, a_total_status_dashboard_t subscribes to two messages:
virtual void so_define_agent() override { so_subscribe( m_status_distrib_mbox ) .event( &a_total_status_dashboard_t::evt_machine_status ); so_subscribe_self().event< show_dashboard >( &a_total_status_dashboard_t::evt_show_dashboard ); }
The first message, machine_status, is waited for by the agent from a special mailbox to which a_machine_t agents send their machine_status messages. And the second message, show_dashboard, agent a_total_status_dashboard_t sends itself in the form of a periodic message:
virtual void so_evt_start() override { // // . const auto period = std::chrono::milliseconds( 1500 ); m_show_timer = so_5::send_periodic< show_dashboard >( *this, period, period ); }
Here a_total_status_dashboard_t uses the same approach as the a_machine_t agent - initiates a periodic message in its so_evt_start () method, which the SObjectizer automatically calls at the very beginning of a_total_status_dashboard_t.
Processing machine_status is very simple: you just need to save the next piece of information in an associative container:
void evt_machine_status( const machine_status & status ) { m_machine_statuses[ status.m_id ] = one_machine_status_t{ status.m_engine_status, status.m_cooler_status, status.m_engine_temperature }; }
And the show_dashboard handler does not contain anything complicated: just an iteration of the contents of the associative container with printing to the standard output stream:
void evt_show_dashboard() { auto old_precision = std::cout.precision( 5 ); std::cout << "=== The current status ===" << std::endl; for( const auto & m : m_machine_statuses ) { show_one_status( m ); } std::cout << "==========================" << std::endl; std::cout.precision( old_precision ); }
Since the a_total_status_dashboard_t agent is not of great interest and its implementation is very simple, we will not go further into its implementation. Who cares, the full source code a_total_status_dashboard_t can be seen here .
In this implementation of the machine_control example, a bundle of several agents deals with the analysis of information from the a_machine_t agents and the issuance of control commands to the agent machines.
First, it is the a_statuses_analyser_t agent that receives the machine_status messages, analyzes them, and determines that a specific a_machine_t needs some kind of impact.
Secondly, there is a group of a_machine_controller_t type template agents that react to signals from a_statuses_analyser_t and give out this or that effect on a specific machine. So, one agent a_machine_controlle_t responds to the situation when the cooling fan should be turned on and sends the turn_cooler_on message to the corresponding agent a_machine_t. Another agent a_machine_controller_t responds to the situation when the engine should be turned off and sends the message turn_engine_off. Etc.
Generally speaking, such a division into a_statuses_analyser_t and a_machine_controller_t is a clear complication of our example. It would be entirely possible to do with just one agent a_statuses_analyser_t, who himself could analyze the information and send the control commands. Although, most likely, the agent a_statuses_analyser_t would greatly increase in volume at the same time.
Initially, in machaine_control, I wanted to show various SObjectizer features, in particular, the use of template agents and agent priorities, so we decided to divide the logic between a_statuses_analyser_t and a_machine_controller_t.
So, the essence of the interaction a_machine_t, a_statuses_analyser_t and a_machine_controller_t is as follows:
The machine_needs_attention message has the following form:
// , . enum class attention_t { none, engine_cooling_done, engine_cooling_needed, engine_overheat_detected }; // , - . struct machine_needs_attention { // - . const std::string m_id; // . const attention_t m_attention; // (/). const engine_state_t m_engine_status; // (/). const cooler_state_t m_cooler_status; };
The a_statuses_analyser_t agent stores past information about each machine and compares it with new information coming in with the machine_status message. If it is found that the engine requires cooling, or that the engine has overheated, or that the engine has reached a safe temperature, then a_statuses_analyser_t generates a machine_needs_attention message. This message is picked up by the appropriate a_machine_controller_t and the right team will be sent to the appropriate a_machine_t agent.
Offal from agent a_statuses_analyser_t is quite voluminous . But most of them relate to storing and analyzing the current state of the agents-machines. We will not analyze this part in details (if there are any questions, I will answer them in the comments), we will just explain in two words:
In the code, all this takes a decent amount of lines, but there is nothing complicated there.
But that part of the a_statuses_analyser_t agent, which is related to the interaction with SObjectizer, is generally minimal: just a subscription to one single message in so_define_agent () and one single event for this message:
virtual void so_define_agent() override { so_subscribe( m_status_distrib_mbox ).event( &a_statuses_analyzer_t::evt_machine_status ); } void evt_machine_status( const machine_status & status ) { auto it = m_last_infos.find( status.m_id ); if( it == m_last_infos.end() ) // . // . it = m_last_infos.insert( last_info_map_t::value_type { status.m_id, last_machine_info_t { attention_t::none, status.m_engine_temperature } } ).first; handle_new_status( status, it->second ); }
Where the handle_new_status () method, which is called inside evt_machine_status (), is already part of the application logic for monitoring the status of an agent-machine, which we briefly described earlier.
The presence of several instances of the a_machine_controller_t agents has the following meaning: when the a_statuses_analyser_t agent determines, for example, that the engine of a car has cooled to a safe temperature, then we may need to do several different actions. First, turn off the cooling fan. Secondly, it may be that the engine was previously disabled and we need to turn it on again.
If we put all this logic inside a_statuses_analyser_t, then its code would grow and it would be more difficult to understand it. Instead, we simply made a_statuses_analyser_t announce what exactly should be noted in the work of the agent machine. And the agents of a_machine_controller_t will already respond to this ad. And each a_machine_controller_t itself determines whether it needs to react or not. If necessary, a_machine_contoller_t receives the machine_needs_attention message and initiates the appropriate command to the agent machine. If not needed, a_machine_controller_t simply ignores the machine_needs_attention message.
The code of the a_machine_controller_t template agent is small, so for ease of working with it, we’ll give the full code for this agent in its entirety:
template< class LOGIC > class a_machine_controller_t : public so_5::agent_t { public : a_machine_controller_t( context_t ctx, so_5::priority_t priority, so_5::mbox_t status_distrib_mbox, const machine_dictionary_t & machines ) : so_5::agent_t( ctx + priority ) , m_status_distrib_mbox( std::move( status_distrib_mbox ) ) , m_machines( machines ) , m_logic() {} virtual void so_define_agent() override { so_set_delivery_filter( m_status_distrib_mbox, [this]( const machine_needs_attention & msg ) { return m_logic.filter( msg ); } ); so_subscribe( m_status_distrib_mbox ) .event( [this]( const machine_needs_attention & evt ) { m_logic.action( m_machines, evt ); } ); } private : const so_5::mbox_t m_status_distrib_mbox; const machine_dictionary_t & m_machines; const LOGIC m_logic; };
So, this is a template class that is parameterized by one parameter: the type of application logic that a_machine_controller_t should have. This type of LOGIC must be of the following type with two methods:
struct LOGIC { bool filter( const machine_needs_attention & msg ) const; void action( const machine_dictionary_t & machines, const machine_needs_attention & evt ) const; };
If there were concepts in C ++ 11, then it would be possible to declare the corresponding concept in order to make it easier to determine which type can be a parameter for the a_machine_controller_t template, and which one can not. But, since in C ++ 11 the concepts were not delivered, then you have to rely on duck typing.
The a_machine_controller_t agent creates an instance of the LOGIC type inside itself and delegates all its actions to this instance. And this instance is nothing more than a controller that provides control actions to the agent machine.
The controller has only two actions:
First, you need to filter messages that are not interesting to a particular controller. a_machine_controller_t :
so_set_delivery_filter( m_status_distrib_mbox, [this]( const machine_needs_attention & msg ) { return m_logic.filter( msg ); } );
— SObjectizer-. , T M, T, , .
so_set_delivery_filter() , -, M , T. true, . false, , , .
. Subscriber-1 mbox-. Subscriber-2 , .
LOGIC::filter() , machine_needs_attention, .
-, - machine_needs_attention, , .
LOGIC::action(). a_machine_controller_t machine_needs_attention:
so_subscribe( m_status_distrib_mbox ) .event( [this]( const machine_needs_attention & evt ) { m_logic.action( m_machines, evt ); } );
, , a_machine_controller_t, . , turn_engine_off, :
struct engine_stopper_t { bool filter( const machine_needs_attention & msg ) const { return msg.m_attention == attention_t::engine_overheat_detected; } void action( const machine_dictionary_t & machines, const machine_needs_attention & evt ) const { so_5::send< turn_engine_off >( machines.find_mbox( evt.m_id ) ); } };
Those. machine_needs_attention, . , - turn_engine_off.
:
struct cooler_starter_t { bool filter( const machine_needs_attention & msg ) const { return (msg.m_attention == attention_t::engine_overheat_detected || msg.m_attention == attention_t::engine_cooling_needed) && msg.m_cooler_status == cooler_state_t::off; } void action( const machine_dictionary_t & machines, const machine_needs_attention & evt ) const { so_5::send< turn_cooler_on >( machines.find_mbox( evt.m_id ) ); } };
. , , .
:
, a_machine_controller_t — .
machine_control , machine_needs_attention . , machine_needs_attention attention_t::engine_overheat_detected cooler_status_t::off, : engine_stopper_t, cooler_starter_t.
, . , engine_overheat_detected turn_engine_off, turn_cooler_on. - , . , . , .
SObjectizer- ?
. , . , . . , , .
SObjectizer : , . so_5::disp::prio_one_thread::strictly_ordered. a_machine_controller_t .
- :
, a_machine_controller_t :
a_machine_controller_t( ..., so_5::priority_t priority, ... ) : so_5::agent_t( ctx + priority ) , ... {}
:
coop.make_agent_with_binder< a_machine_controller_t< engine_stopper_t > >( disp->binder(), so_5::prio::p4, status_distrib_mbox, machines ); coop.make_agent_with_binder< a_machine_controller_t< cooler_starter_t > >( disp->binder(), so_5::prio::p3, status_distrib_mbox, machines );
so_5::prio::p4 so_5::prio::p3 — .
, a_machine_t st_engine_off, .. . , . , a_machine_t, a_machine_controller_t ?
, - -, turn_engine_on -.
, C++ , a_machine_t, a_total_status_dashboard_t, a_statuses_analyser_t a_machine_controller_t. , ad-hoc . Those. , -. machine_control :
coop.define_agent().on_start( [&dict] { dict.for_each( []( const std::string &, const so_5::mbox_t & mbox ) { so_5::send< turn_engine_on >( mbox ); } ); } );
define_agent() ad-hoc , . ad-hoc SObjectizer-: turn_engine_on.
SObjectizer-, , .
. - . SObjectizer . , .
machine_control , :
, , , : machine_dictionary . , - - .
, - ( machine_status machine_needs_attention) . SObjectizer , , , turn_engine_on, . machine_dictionary.
, , , .
, , , , a_machine_t a_machine_controller_t, .
SObjectizer-, ad-hoc — . , , - SObjectizer, , , , « ». -, , ping-pong , , . , SObjectizer.
, , . , , Actor Model Publish/Subscribe .
machine_control , (.. ) .
, a_machine_t machine_status , , . a_machine_t ( ) . , .
a_machine_t ( turn_engine_on), , . a_machine_t , . , — a_machine_t .
, - turn_engine_on a_machine_t, Actor Model, 1:1.
machine_status machine_needs_attention Actor Model, .. 1:N. Publish/Subscribe. , machine_status machine_needs_attention, . — - Publish. Subscribe, , , a_total_status_dashboard_t, a_statuses_analyser_t a_machine_controller_t so_define_agent().
, SObjectizer: . , SObjectizer multi-producer/multi-consumer , 1:N.
, machine_status, , , . a_total_status_dashboard_t machine_status . a_statuses_analyser_t machine_status a_machine_t.
. , . a_total_status_dashboard_t a_statuses_analyser_t. a_total_status_dashboard_t - a_gui_status_dashboard_t, std::cout, . , , , machine_status.
SObjectizer — , , . SObjectizer , ( CSP- ).
SObjectizer , , . (.. ), .
, . — . , . -, - , so_5::signal_t, . , - , - . , , - - .
, , .
, SObjectizer- so_5_extra , SObjectizer. , SObjectizer , , . so_5_extra , . - SObjectizer, . SObjectizer, so_5_extra.
Source: https://habr.com/ru/post/332166/
All Articles