Just take a look at SObjectizer if you want to use Actors or CSP in your C++ project

в 8:06, , рубрики: actor model, actors, c++, c++17, concurrency, open source, sobjectizer, Программирование

Just take a look at SObjectizer if you want to use Actors or CSP in your C++ project - 1

A few words about SObjectizer and its history

SObjectizer is a rather small C++ framework that simplifies the development of multithreaded applications. SObjectizer allows a developer to use approaches from Actor, Publish-Subscribe and Communicating Sequential Processes (CSP) models. It's an OpenSource project that is distributed under BSD-3-CLAUSE license.

SObjectizer has a long history. SObjectizer itself was born in 2002 as SObjectizer-4 project. But it was based on ideas from previous SCADA Objectizer that was developed between 1995 and 2000. SObjectizer-4 was open-sourced in 2006, but its evolution was stopped soon after that. A new version of SObjectizer with the name SObjectizer-5 was started in 2010 and was open-sourced in 2013. The evolution of SObjectizer-5 is still in progress and SObjectizer-5 has incorporated many new features since 2013.

SObjectizer is more or less known in the Russian segment of the Internet, but almost unknown outside of the exUSSR. It's because the SObjectizer was mainly used for local projects in exUSSR-countries and many articles, presentations, and talks about SObjectizer are in Russian.

A niche for SObjectizer and similar tools

Multithreading is used in Parallel computing as well as in Concurrent computing. But there is a big difference between Parallel and Concurrent computing. And, as a consequence, there are tools targeted Parallel computing, and there are tools for Concurrent computing, and they are different.

Roughly speaking, Parallel computing is about using several cores to reduce calculation times. For example, transcoding a video file from one format to another can take one hour on one CPU core, but just 15 minutes on four CPU cores. Tools like OpenMP, Intel TBB, HPX or cpp-taskflow are designed to be used in Parallel computing. And those tools support appropriate for that area approaches, like task-based or data-flow programming.

Concurrent computing is about dealing with many (probably different) tasks at the same time. Database server or MQ-broker can be good examples: a server has to accept a connection, read and parse data from accepted connections, handle received requests (performing several actions for every request), sending responses and so on. Strictly speaking, there is no need to use multithreading in concurrent computing: all those tasks can be performed on just one worker thread. But usage of multithreading and several CPU cores can make your application more performant, scalable and responsive.

Approaches like Actor Model or CSP are intended for dealing with Concurrent computing. Good examples of usage Actors in Concurrent computing area are InfineSQL project and Yandex Message-Queue. Both of those projects use actors inside.

So the tools like SObjectizer, QP/C++ or CAF, those support Actor Model, are useful in solving tasks from Concurrent computing area. It means that usage of SObjectizer probably won't give you anything in tasks like conversion of video streams. But you can get a very different result implementing a message broker on top of SObjectizer.

Disclaimer

Usage of Actor- or CSP models can give you huge benefits in some tasks, but there are no guarantees that those models are appropriate for your particular problem. The talk about applicability of Actor- or CSP models is beyond the scope of that article. Let's assume that Actor or/and CSP model is applicable for your tasks and you know how to use them efficiently.

What SObjectizer can give to a user?

Shared-nothing and fire-and-forget principles just out of box

Usage of Actors assumes the absence of any shared data. Every actor owns its data and this data is not visible to anyone else. This is shared-nothing principle that is well known in distributed application development, for an example. In multithreaded application shared-nothing principle has an important benefit: it allows to avoid such dangerous problems for work with shared data as deadlocks and data-races.

Interaction between actors (agents) in SObjectizer is performed only via asynchronous messages. One agent sends a message to another agent and this operation doesn't block the sender (in a common case).

Asynchronous interaction allows using another useful principle: fire-and-forget. When some agent needs some operation to be done, it sends (fires) a message and continues its work. In most cases, the message will be received and processed.

For example, there can be an agent that reads accepted connections and parses incoming data. If the whole PDU is read and parsed the agent just sends that PDU to another agent-processor and returns to reading/parsing new incoming data.

Dispatchers

Dispatchers are one of the cornerstones of SObjectizer. Dispatchers provide a working context (aka worker thread) on that an agent will handle incoming messages. Instead of creating worker threads (or pools of threads) manually a user creates dispatchers and binds agents to them. A user can create as many dispatchers in an application as he/she wants.

The best thing with dispatchers and agents in SObjectizer is the separation of concepts: dispatchers are responsible for managing working context and own message queues, agents perform application logic and do not bother about worker context. It allows moving an agent from one dispatcher to another literally by a click. Yesterday an agent worked on one_thread dispatcher, today we can rebind it to active_obj dispatcher, and tomorrow we can rebind it to thread_pool dispatcher. Without changing a line in the agent's implementation.

There are eight types of dispatchers in SObjectizer-5.6.0 (and another one can be found in so5extra companion project): starting from very simple ones (one_thread or thread_pool) to sophisticated ones (like adv_thread_pool or prio_dedicated_threads::one_per_prio). And a user can write its own dispatcher for specific conditions.

Hierarchical state machines are built-in functionality

Agents (actors) in SObjectizer are state machines: the reaction on an incoming message depends on the current state of the agent. SObjectizer supports most of the hierarchical state machines (HSM) features: nested states, deep- and shallow history for a state, on_enter/on_exit handlers, time limits for staying in a state. Only orthogonal states are not supported in SObjectizer now (we didn't see a necessity of that feature in our projects, and nobody asked us to add support for that feature).

CSP-like channels just out of box

There is no need to use SObjectizer's agents (aka actors). The whole application can be developed just using std::thread objects and SObjectizer's mchains (aka CSP-channels). In that case application development with SObjectizer will be somewhat similar to the development in Go language (including an analog of Go's select construct that allows to waits messages from several channels).

SObjectizer's mchains can have a very important feature: incorporated back-pressure mechanism. If a user creates a size-limited mchain and then tries to push a message into the full mchain the send operation can block sender for some time. It allows to solve a famous problem with a fast producer and a slow consumer.

SObjectizer's mchains have another interesting feature: a mchain can be used as very simple load distribution tool. Several threads can wait on receive from the same mchain at the same time. If a new message is sent to that mchain just one thread will read and handle that message.

Only a part of an application can use SObjectizer

There is no need to use SObjectizer in every part on an application. Just a part of an application can be developed by using SObjectizer. So if you already use Qt or wxWidgets, or Boost.Asio as the main framework for your application it is possible to use SObjectize in just one submodule of your app.

We had experience on usage of SObjectizer for development of libraries which hide the usage of SObjectizer as an implementation detail. The public API of those libraries didn't expose the presence of SObjectizer at all. SObjectizer was entirely under the control of a library: the library started and stopped SObjectizer as it needed. Those libraries were used in applications that were completely unaware of the presence of SObjectizer.

If SObjectizer is used only in a part of an application there is a task of communication between SObjectizer- and non-SObjectizer-parts of the application. This task is easily solved: messages from a non-SObjectizer-part to SObjectizer-part can be sent via ordinary message-delivery SObjectizer's mechanism. Messages in the opposite direction can be delivered via mchains.

You can run several instances of SObjectizer at the same time

SObjectizer allows run of several instances of SObjectizer (called SObjectizer Environment) in one application at the same time. Every SObjectizer Environment will be independent of other such environments.

This feature is invaluable in situations where you have to build an application from several independent modules. Some modules can use SObjectizer, some don't. Those modules that require SObjectizer can run its copy of SObjectizer Environment and that won't have an influence on other modules in the application.

Timers are part of SObjectizer

Support of timers in the form of delayed and periodic messages is another of the cornerstones of SObjectizer. SObjectizer has several implementations of timer mechanisms (timer_wheel, timer_heap, and timer_list) and can handle tens, hundreds and thousands of millions of timers in an application. A user can choose the most appropriate timer mechanism for an application. Moreover, a user can provide its own implementation of timer_thread/timer_manager if none of the standard ones is appropriate for the user's conditions.

SObjectizer has various customization points and tuning options

SObjectizer allows customization of several important mechanisms. For example, a user can select one of the standard implementations of timer_thread (or timer_manager). Or can provide its own implementation. A user can select an implementation of lock objects used by message queues in SObjectizer's dispatchers. Or can provide its own implementation.

A user can implement its own dispatcher. A user can implement its own message box. A user can implement its own message envelope. A user can implement its own event_queue_hook. And so on.

Where SObjectizer can or can't be used?

It's much easier to say where SObjectizer can't be used by objective reasons. So we start the discussion by enumerating such areas and then we'll give some examples of the usage of SObjectizer in the past (and not only in the past).

Where SObjectizer can't be used?

As has been said above Actor- and CSP-models is not a good choice for high-performance computing and other areas of Parallel computing. So if you have to multiple matrixes or transcode video streams then tools like OpenMP, Intel TBB, cpp-taskflow, HPX or MPI will be more suitable.

Hard real-time systems

Despite the fact that SObjectizer has its roots in SCADA-systems the current implementation of SObjectizer (aka SObjectizer-5) can't be used in hard real-time systems. It is mainly because of the usage of dynamic memory in SObjectizer implementation: messages are dynamically allocated objects (however, SObjectizer can use preallocated objects as messages), dispatchers use dynamic memory for message queues, even time-limits for agent's states use dynamically allocated objects to perform time-checking.

Unfortunately, the term "real-time" is heavily overused in the modern world. It is often said about real-time web services, like "real-time web application" or "real-time web analytics" and so on. The term "on-line" or "live" is more appropriate for such applications than the term "real-time", even in "soft real-time" form. Thus, if we speak about something like "real-time web application" then SObjectizer can easily be used in such "real-time" systems.

Constrained embedded systems

SObjectizer relies on the С++ standard library: std::thread is used for thread management, std::atomic, std::mutex, std::condition_variable are used for data synchronization, RTTI and dynamic_cast are used insize SObjectizer (for example, std::type_index are used for message type identification), C++ exceptions are used for error reporting.

It means that SObjectizer can't be used in environments where such facilities of the standard library are not available. For example, in the development of constrained embedded systems where only a part of C++ and C++ stdlib can be used.

Where SObjectizer was used in the past?

Now we try to speak briefly about some use-cases of SObjectizer's usage in the past (and not only in the past). Unfortunately, it isn't full information because there are some problems.

First of all, we don't know about all the usages of SObjectizer. SObjectizer is free software that can be used even in proprietary projects. So some people just get SObjectizer and use it without providing any feedback for us. Sometimes we acquire some information about SObjectizer's usage (but without any details), sometimes we know nothing.

The second problem is permission to share information about the usage of SObjectizer in a particular project. We have received that permission very rarely, in most cases users of SObjectizer do not want to open implementation details of their projects (sometimes we understand the reasons, sometimes don't).

We apologize for the fact that the information provided looks so scarce and does not contain any details. Nevertheless, there are some examples of usage of SObjectizer:

  • SMS/USSD aggregation gateway that handles more than 500M of messages per month;
  • part of the system serving online payments via ATMs of one of the biggest Russian banks;
  • simulation modeling of economic processes (as part of Ph.D. research);
  • distributed data acquisition and analytic system. Data collected on points distributed worldwide by the commands from the central node. MQTT was used as a transport for control and acquired data distribution;
  • testing environment for checking real-time control system for railway equipment;
  • automatic control system for theatre scenery. More details can be found here;
  • components of data management platform in an online advertising system.

A taste of SObjectizer

Let's see several simple examples to take some taste of SObjectizer. Those are very simple examples which, we hope, do not require additional explanations excluding the comments in the code.

The traditional "Hello, World" example in Actor Model's style

The simplest example with just one agent that reacts to hello message and finishes its work:

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

Another version of "Hello, World" with agents and Publish/Subscribe model

The simplest example with several agents, all of them react to the same instance of hello message:

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

If we run that example we can receive something like that:

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

"Hello, World" example in CSP-style

Let's look at an example of SObjectizer without any actors, just std::thread and CSP-like channels.

Very simple version

This is a very simple version that is not exception safe:

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

More robust, but still simple version

This is a modified version of the example shown above with the addition of exception safety:

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

A rather simple HSM example: blinking_led

This is a standard example from SObjectizer's distribution. The main agent of this example is a HSM that can be described by the following statechart:

blinking_led statechart

The source code of the example:

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

Timers, overload control for an agent and active_obj dispatcher

Overload control is one of the main problems for actors: message-queues for actors are unlimited usually and this can lead to uncontrolled growth of queues if a fast message producer sends messages quicker then the receiver can handle them. The following example shows such SObjectizer's feature as message limits. It allows to limit count of messages in the agent's queue and defend the receiver from redundant messages.

This example also shows the usage of the timer in the form of a periodic message. The binding of agents to the active_obj dispatcher is also shown there. Binding to that dispatcher means that every agent of the coop will work on own worker thread (e.g. an agent becomes an active object).

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

If we run that example we can see the following output:

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

This output shows that several messages that can't fit into the defined limit are rejected and redirected to another receiver.

More examples

An example that more or less similar to the code from real-life applications can be found in our Shrimp demo project. Another set of interesting examples can be found in this mini-serie about classical "dining philosophers problem": part 1 and part 2. And, of course, there are a lot of examples in SObjectizer itself.

What about the performance?

There is a very simple answer: it is more than good enough for us. SObjectizer can distribute millions of messages per second, and the actual speed depends on the types of used dispatchers, message kinds, load profile, hardware/OS/compiler used and so on. In a real application, we usually use just a fraction of SObjectizer speed.

The performance of SObjectizer for your particular task highly depends on your task, the particular solution of that task, on your hardware or virtual environment, on the version of your compiler and your OS. So the best way to find an answer to that question is to create own benchmark that will be specific to your task and experiment with it.

If you want numbers from some synthetic benchmarks then there are some programs in test/so_5/bench folder of SObjectizer distribution.

A note about comparison with different tools

We think that a benchmarking game comparing the speed of different tools is a marketing game. We did an attempt in the past but quickly realized that it is just a waste of our time. So we don't play that game now. We spend our time and our resources only on benchmarks which allow us to check the absence of performance degradation, to resolve some corner cases (like performance of MPMC mboxes with big amount of subscribers or performance of an agent with hundreds of thousands of subscriptions), to speed up some SObjectizer-specific operations (like registration/deregistration of a coop).

So we leave the comparison of speed to those who like that game and have time to play it.

Why SObjectizer does look exactly as it is?

There are several "actor frameworks" for C++, and all of them look different. It seems that it has some objective reasons: every framework has its unique features and targets different goals. Moreover, actors in C++ can be implemented very differently. So the main question is not "why framework X doesn't look like framework Y?", but "why framework X does look as it is?"

Now we'll try to describe some reasons behind the main SObjectizer's features briefly. We hope it allows a better understanding of SObjectizer's abilities. But before we start it is necessary to mention one very important thing: SObjectizer has never been an experiment. It was created for solving real-life working and it has been evolving based on the real-life experience.

Agents are objects of classes derived from agent_t

Agents (aka actors) in SObjectzer are objects of user-defined classes which must be derived from a special class agent_t. It may look redundant in tiny toy examples, but our experience shows that approach greatly simplifies the development of real software where agents usually have the size in several hundred of lines (you can see one of the examples here, but this blog-post is in Russian). Sometimes even in several thousands of lines.

The experience shows us that a simple agent with the first version in a hundred lines becomes much fatter and complex in several next years of evolution. So, after five years you can find a monster in a thousand lines with dozens of methods.

Usage of classes allows us to manage the complexity of agents. We can use inheritance of classes. And we can use template classes too. These are very useful techniques that greatly simplify the development of families of agents with similar logic inside.

Messages as objects of user structs/classes

Messages in SObjectizer are objects of user-defined structs or classes. There are at least two reasons for that:

  • the development of SObjectizer-5 started in 2010 when C++11 wasn't standardized yet. So in the beginning, we couldn't use such features of C++11 as variadic templates and std::tuple class. The only choice we had was the usage of an object of a class inherited from a special class message_t. Now there is no need to derive the type of message from message_t, but SObjectizer wraps a user object into message_t-derived object anyway under the hood;
  • content of a message can easily be changed without modification of signatures of event handlers. And there is a control from a compiler: if you remove some field from a message or change its type then the compiler will tell you about wrong access to that field.

Usage of messages as objects also allows to work with preallocated messages and to store a received message to some container and resend it later.

Coops of agents

A coop of agents is probably one of the unique SObjectizer's features. A coop is a group of agents which should be added to and removed from SObjectizer in a transaction manner. It means that if a coop contains three agents then all those agents should be added to SObjectizer successfully or no one of them should be added. Similarly, all three agents should be removed from SObjectizer or all three agents should continue their work.

The need in coops was discovered soon after the start of SObjectizer life. It became obvious that agents would be created by groups, not by single instances. Coops were invented to simplify the life of a developer: there is no need to control the creation of the next agent and remove previously created agents if the creation of a new agent fails.

A coop can also be seen as a supervisor in all-for-one mode: if an agent from the coop fails then the whole coop will be removed from SObjectizer Environment and destroyed (a user can react to that and recreate the coop again).

Message boxes

Message boxes are another unique SObjectizer's feature. Messages in SObjectizer are sent to a message box (mbox), not to an agent directly. There can be one receiver behind the mbox, or there can be a million subscribers, or there can be no one.

Mboxes allows us to support basic functionality of Publish-Subscribe model. An mbox can be seen as MQ-broker and the message type can be seen as a topic.

Mboxes allows us also implement various interesting form of message delivery. For example, there is a round-robin mbox that spreads messages between subscribers in round-robin manner. There is also a retained mbox that holds the last sent message and resend it automatically for every new subscriber. There is also a simple wrapper around libmosquitto that allows to use MQTT as a transport for a distributed application.

Agents as HSM

Agents in SObjectizer are state machines. It was from the very beginning simply because SObjectizer has roots in SCADA field, where state machines are actively used. But it quickly became obvious that agents in the form of a state machine can be useful even in different niches (like telecom and finance applications).

Support of hierarchical state machines (e.g. on_enter/on_exit handlers, nested states, time limits and so on) were added after some time of using SObjectizer in production. And this feature made SObjectizer yet more powerful and convenient tool.

Usage of C++ exceptions

C++ exceptions are used in SObjectizer as the main error reporting mechanism. Despite the fact that the usage of C++ exception can sometimes be costly, we decided to use exceptions instead of error codes.

We had a negative experience with error codes in SObjectizer-4, where exceptions weren't used. This lead to ignorance of errors in application code and sometimes important actions weren't done because there was an error creating a new coop or sending a message. But this error was ignored and that fact was discovered much later.

Usage of C++ exceptions in SObjectizer-5 allows writing more correct and robust code. In usual cases, exceptions are thrown by SObjectizer very rarely so the usage of exception has no negative impact on SObjectizer's performance or the performance of applications written on top of SObjectizer.

No support for distributed applications "out of box"

SObjectzer-5 has no built-in support for distributed applications. It means that SObjectizer distributes messages just inside one process. If you need to organize inter-process or inter-note message distribution then you have to integrate some kind of IPC in your application.

This is not because we can't implement some form of IPC in SObjectizer. We already had that in SObjectizer-4. And because we have such experience we decided not to do that in SObjectizer-5. We learned that there is no one type of IPC that perfectly fits for different conditions.

If you want to have good inter-node communication in your application you have to select appropriate underlying protocols. For example, if you have to spread millions of small packets with some short-living data (like distribution of measurement of the current weather conditions) then you have to use one IPC. But if you have to transfer huge BLOBs (like 4K/8K video streams or archives with financial data inside) then you have to use another IPC type.

And we don't speak about introperability with software written in different languages...

You can believe that some universal "actor framework" can provide you an IPC that will be good for different conditions. But we know that it is just marketing bullshit. Our experience shows us that it is much simpler and much safer to add the IPC you need in your application then to rely on ideas, needs and knowledge of authors of a 3-rd party "actor framework".

SObjectizer allows incorporating various types of IPC in the form of custom mboxes. So it allows hiding the fact of message distribution over a network from a SObjectizer's users.

Instead of the conclusion

The SObjectizer framework is not a big one, but it isn't a small one. So it is impossible to give the reader a rather deep impression about SObjectizer in just one overview. Because of that, we invite you to take a look at the SObjectizer project.

SObjectizer itself lives on BitBucket with the official mirror on GitHub. There is the project's Wiki on BitBucket and we recommend to start from SObjectizer 5.6 Basics and then go to articles from In-depth series. For those who want to go deeper, we can recommend Let's look under the SObjectizer's hood section.

If you have any questions you can ask us in SObjectizer's group on Google's groups.

Автор: eao197

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js