четверг, 28 мая 2015 г.

[prog.c++11] SObjectizer-5.5 compared to C++ Actor Framework

There are two points of view from which a comparison could be made:

  1. Top-level view on the purposes of each frameworks and primary targets of framework’s developers.
  2. Low-level view on implemented features and technical details of the implementations.

From the top-level point of view it is obvious that each frameworks have different roots and it seems that authors have different targets.

CAF looks like an very successful attempt of porting Erlang-style actors to modern C++. Traces of this could be seen in actor spawning/monitoring features and, especially, in pattern-matching implementation. As result of that CAF-based code looks like an Erlang (or Scala+Akka) program, but written in C++ syntax.

Another similarity to Erlang could be seen in network transparency implemented in CAF. It means that CAF already have facilities for building actor-based distributed applications.

The similarity to Erlang has its cost: CAF is very demanding to the level of C++11 support in a compiler. As result CAF is working only on GCC and Clang compilers and has native support of Windows only via MinGW.

It also seems that one of main targets for CAF is the efficient utilization of modern many core hardware via lightweight agents and very fast message exchanges. In conjunction with abilities of utilizing GPU it looks that parallel computing is one of main fields of CAF application. So I think that in message throughput benchmark CAF will show more higher results than SObjectizer (Upd. It is not true, at least for SO-5.5.15 and CAF-0.14.4).

In contrast the actor model was not a main model during SObjectizer development. SObjectizer is based on several ideas most of them were formulated long time ago -- somewhere between 1995-1997 when Erlang and Erlang-style actor model were not widely known.

SObjectizer was born as result of an attempt of usage of OOP-approach in SCADA-applications. Because of that complex agents in SObjectizer often looks like Qt/MFC/wxWidget objects with a lot of message handlers, but not as Erlang-style actors. However an agent is SObjectizer is also a lightweight entity so you can easily create millions or tens of millions of agents in an application.

One of the main targets in SObjectizer development is a portability. SObjectizer has to support Visual C++ natively so our abilities of using modern C++11/14 facilities is very limited. As a consequence the SObjectizer-related code could be more verbose.

Network transparency was removed from SObjectizer-5 so the SObjectizer-5 itself has no facilities to build distributed applications. Externals libraries should be used in that case. This is a conscious decision. There are different application domains, the very different requirements and environments. Sometimes a very simple protocol on the top of plain TCP (or even UDP) sockets could be appropriate. Sometimes it is necessary to use AMQP and XML-based protocol for communication with Enterprise Java components.

I think it is correct to say that SObjectizer is oriented primarily to concurrent programming. There is no such target as high level of CPU utilization. Instead there is a target to provide useful tools for solving concurrency-related tasks (sometimes usage of SObjectizer is similar to usage of SEDA-way).

One of those tools is a dispatcher. A dispatcher is an object that is responsible for providing working context for agents. Every agent should be bound to a dispatcher which is more appropriate for the agent tasks. There are several ready-to-use dispacthers in SObjectizer (from very simple ones with just one working threads to more complex with thread pools and possibility to distinguish thread-safe and thread-unsafe event-handlers). A user could create and launch as many dispatchers as he needs for solving his task.

Another important tool is a mbox (message box). In SObjectizer messages are sent not to agents (like in CAF/Erlang/Akka) but to mboxes. There could be several agents behind the mbox. Or just one. Or no one. Mboxes in SObjectizer is a consequence of usage another model during SObjectizer development -- Publish/Subscribe.

Usage of mboxes makes some scenarios very simple to implement in SObjectizer. For example some temperature sensor could send its current value to multi-consumer mbox as a message. And this message will be delivered to several different receivers.

There are also single-consumer mboxes which are created automatically for every agent. Usage of single-consumer mboxes is very similar to CAF/Erlang-like usage of actors as the destination to a message. But all mboxes in SObjectizer have the same interface so it is easy to change multi-consumer to single-consumer (or vise versa) mboxes and this will be transparent for a message sender.

There is yet another important difference between CAF and SObjectizer. It seems that CAF is designed to have only one instance of CAF run-time in the application. SObjectizer in contrast allows to have several SObjectizer run-times running at the same time inside one applications. Every run-time instance (named Environment) will work independently from others. This could be useful for using SObjectizer in a library implementation. There were cases when SObjectizer was used in implementation of several libraries. Every library hides the usage of SObjectizer so a user of library have no knowledge about that. An ability to run several independent SObjectizer Environment at the same time allows to seamlessly integrate those libraries into one application.

Low-level differences look like a consequence of top-level differences.

Almost all agents in SObjectizer-based application are instances of classes derived from a special base class. There are also ad-hoc agents, but they used not so widely and definition of ad-hoc agents has nothing in common with simple function-based actors in CAF.

Because SObjectizer doesn’t support pattern-matching for messages all messages in SObjectizer must be represented as instances of dedicated structs/classes (or tuples).

There is no such thing as atoms in SObjectizer. But there is a special case for messages without actual data inside -- those are called ‘signals’ and represented as empty struct derived from a special class.

There is a different way for dealing with synchronous interactions. This is not only a completely different syntax, but also an ability to receive std::future<T> object as result of synchronous invocation.

There is no links between agents in SObjectizer. And there are no groups of actors like in CAF. But there is such thing as a ‘coop’. Coop is a group of tightly related agents. All agents from a coop is registered and deregistered in a transactional model. Coops could have parent-child relationship. When a parent coop is deregistered all its child coops are deregistered automatically.

And it seems that there is different approaches for handling exceptions from actor/agents in CAF and SObjectizer.

SObjectizer doesn’t support message priorities yet (this feature is planned to next major version 5.6.0 and it seems that support of priorities will be completely different from one in CAF). But SObjectizer has such thing as message limits. Limits allows to implement very simple forms of overload control for agents.

There is a such thing as message delivery filters in SObjectizer. This is a consequence of usage Pub/Sub model as one of the main ideas during SObjectizer development. Delivery filters control what messages have to be stored in the event-queue of a receiver.

I suppose there also a lot of more small (and, probably, big ones) differences. So it is better to take a lock to a small example implemented by different frameworks. This is fixed stack example from CAF documentation. But it is necessary to mention that this example is not typical for SObjectizer. Agents in SObjectizer-based applications usually much larger and complex ones. To make an impression on how agents can look in real-world applications it is better to look at the examples related to the Producer/Consumer tasks or message limits.

CAF fixed_stack SObjectizer-5.5.5 fixed_stack
using pop_atom = atom_constant<atom("pop")>;
using push_atom = atom_constant<atom("push")>;

class fixed_stack : public sb_actor<fixed_stack> {
 public:
  fixed_stack(size_t max) : max_size(max)  {
    full.assign(
      [=](push_atom, int) {
        /* discard */
      },
      [=](pop_atom) -> message {
        auto result = data.back();
        data.pop_back();
        become(filled);
        return make_message(ok_atom::value, result);
      }
    );
    filled.assign(
      [=](push_atom, int what) {
        data.push_back(what);
        if (data.size() == max_size) {
          become(full);
        }
      },
      [=](pop_atom) -> message {
        auto result = data.back();
        data.pop_back();
        if (data.empty()) {
          become(empty);
        }
        return make_message(ok_atom::value, result);
      }
    );
    empty.assign(
      [=](push_atom, int what) {
        data.push_back(what);
        become(filled);
      },
      [=](pop_atom) {
        return error_atom::value;
      }
    );
  }

  size_t max_size;
  std::vector<int> data;
  behavior full;
  behavior filled;
  behavior empty;
  behavior& init_state = empty;
};
class fixed_stack : public so_5::rt::agent_t
{
  so_5::rt::state_t st_empty = so_make_state();
  so_5::rt::state_t st_filled = so_make_state();
  so_5::rt::state_t st_full = so_make_state();
 
  const size_t m_max_size;
  std::vector< int > m_stack;
 
public :
  fixed_stack( context_t ctx, size_t max_size )
    : so_5::rt::agent_t( ctx ), m_max_size( max_size )
    {}
 
  struct msg_push : public so_5::rt::message_t
  {
    int m_val;
    msg_push( int v ) : m_val( v ) {}
  };
  struct msg_pop : public so_5::rt::signal_t {};
 
  virtual void so_define_agent() override
  {
    this >>= st_empty;
 
    so_subscribe_self().in( st_empty ).in( st_filled )
      .event( [this]( const msg_push & w ) {
          m_stack.push_back( w.m_val );
          this >>= (m_stack.size() == m_max_size ?
              st_full : st_filled);
        } );
 
    so_subscribe_self().in( st_filled ).in( st_full )
      .event< msg_pop >( [this]() {
          auto r = m_stack.back();
          m_stack.pop_back();
          this >>= ( m_stack.empty() ? st_empty : st_filled );
          return r;
        } );
 
    so_subscribe_self().in( st_empty )
      .event< msg_pop >( []() {
          throw std::runtime_error( "empty_stack" );
        } );
  }
}; 

Комментариев нет: