Skip to content

Customize and Spawn an Actor

zzxx edited this page Oct 9, 2021 · 4 revisions

Class-based Customization

To customize an actor, one way is to create new actor class that inherts ActorBehavior. The new actor class can override the following functions:

// Define a set of message handlers to process different kinds of incoming messages.
MessageHandlers behavior();
// Initialize the actor after the actor is constructed.
void start();
// Clean up the actor before the actor is destroyed.
void stop();

To define a MessageHandlers, we need to pass a list of Code - MessageHandler mappings. The code of each message handler must be different. A message will be processed by the mesasge handler that shares the same code, where the argument types of the message handler must match with the element types of the messages, i.e., argument type using auto is not allowed and no type conversion on the message elements will happen. The message handler can be a lambda, a functor or a function pointer. Here is an example:

struct FunctorX {
  void operator()(int x) { ... }
};

struct NewActor : public ActorBehavior {
  static constexpr Code Plus{0};
  static constexpr Code Minus{1};
  static constexpr Code MemFunc{2};
  static constexpr Code Functor{3};
  static constexpr Code Termination{4};

  void member_function() { ... }

  MessageHandlers behavior() override {
    return {
      Plus  - [&](int a, int b) { this->reply(Plus,  a + b); },
      Minus - [&](int a, int b) { this->reply(Minus, a - b); },
      MemFunc - std::bind(NewActor::member_function, this),
      Functor - FunctorX{},
      Termination - [&]() { this->deactivate(); }
    }
  }
};

To spawn an actor, we pass the actor class to ActorSystem::spawn:

// 1. create an actor system
ActorSystem actor_system;
// 2. spawn the new actor
Actor new_actor = actor_system.spawn<NewActor>();

That ActorSystem::spawn creates an object of NewActor. Underneath it creates a thread that executes ActorBehavior::launch() with the actor object, which is basically to wait for incoming messages and pass the messages to the MessageHandlers defined in behavior(). The return value of ActorSystem::spawn, i.e., Actor new_actor, is a handle used to send messages to the this new actor.

A class-based actor terminates when ActorBehavior::deactivate() is called, where stop() is called and the actor object will be destroyed.

Lambda-based Customization

Another way to customize an actor is to describe the behavior of an actor using a lambda:

Actor new_actor = actor_system.spawn([&](ActorBehavior& self) {
  /* send or receive messages here */
});

A lambda-based actor terminates when the invocation of the lambda finishes.

ScopedActor

Class-based actors and lambda-based actors are running in a context that is different from where they are spawned, i.e., they should not access the context where they are spawned for thread-safety purpose. For some cases, we may want to send something in the current context to actors. That is what ScopedActor is designed for.

ScopedActor is created by ActorSystem::spawn_scoped_actor. It is essentially a wrapper of a pointer of an ActorBehavior object and this ActorBehavior object is not running. This ActorBehavior object is destroyed when ScopedActor is destroyed, which usually happens when we go out of the scope that spawns the ScopedActor.

Here is an example:

Actor worker = actor_system.spawn([&](ActorBehavior& self) {
  worker.receive({
    StartToWork - [&]() {
      // start to do the work when told to do so
    }
  });
});

{
  ScopedActor notifier = actor_system.spawn_scoped_actor();
  notifier->send(worker, StartToWork);
} // the ScopedActor is destroyed now

Spawn actors with ActorEngine

ActorEngine runs the actors with a set of threads, where the number of threads is independent from the number of actors. Obviously, ActorEngine only allows class-based actors.

// Use 4 threads
ActorEngine engine{actor_system, 4};
// Spawn a NewActor inside the engine.
Actor new_actor = engine.spawn<NewActor>();

Underneath, ActorEngine distributes the actors to the threads and each thread listens to the messages that are sent to the actors it manages. It also provides a simple load balance strategy which migrates the actors among the threads in order to make the loads of threads balance (see example here).

ActorEngine is only one of the choices for running actors. Users may create their own ActorEngine that runs actors with different strategies.

Clone this wiki locally