Actors

Actors in CAF are a lightweight abstraction for units of computations. They are active objects in the sense that they own their state and do not allow others to access it. The only way to modify the state of an actor is sending messages to it.

CAF provides several ways to implement actors, each covering a particular use case. We distinguish between these characteristics: (1) dynamically or statically typed, and (2) state-based or function-based. These characteristics can be combined freely. For example, an actor can have dynamically typed messaging and implement a state class.

Dynamically typed actors are more familiar to developers coming from Erlang or Akka. They (usually) enable faster prototyping but require extensive unit testing. Statically typed actors require more source code but enable the compiler to verify communication between actors. Since CAF supports both, developers can freely mix both kinds of actors to get the best of both worlds. A good rule of thumb is to make use of static type checking for actors that are visible across multiple translation units.

Actors in CAF are event-based, which means that they are scheduled cooperatively. The implementation is lightweight with a memory footprint of only few hundred bytes per actor. Developers can exclude—detach—event-based actors that potentially starve others from the cooperative scheduling while spawning it. A detached actor lives in its own thread of execution.

Environment / Actor Systems

All actors live in an actor_system representing an actor environment including Scheduler, Registry, and optional components such as a Middleman. A single process can have multiple actor_system instances, but this is usually not recommended (a use case for multiple systems is to strictly separate two or more sets of actors by running them in different schedulers). For configuration and fine-tuning options of actor systems see Configuring Actor Applications. A distributed CAF application consists of two or more connected actor systems. We also refer to interconnected actor_system instances as a distributed actor system.

Common Actor Types

The following pseudo-UML depicts the class diagram for actors in CAF. Irrelevant member functions and classes as well as mixins are omitted for brevity. Selected individual classes are presented in more detail in the following sections.

Actor Types in CAF

Class local_actor

The class local_actor is the root type for locally executed actors in CAF. It defines all common operations. However, users of the library usually do not interact with this class directly and instead use one of the derived classes event_based_actor or typed_event_based_actor. The following table also includes member function inherited from abstract_actor (implementation details that we only mention here for the sake of completeness).

Types

mailbox_type

A concurrent, many-writers-single-reader queue type.

Constructors

(actor_config&)

Constructs the actor using a config.

Observers

actor_addr address()

Returns the address of this actor.

actor_system& system()

Returns context()->system().

actor_system& home_system()

Returns the system that spawned this actor.

Actor Management

link_to(other)

Links to other (see Link).

unlink_from(other)

Remove the link to other.

spawn(F fun, xs...)

Spawns a new actor from fun.

spawn(actor_from_state<T>, ...)

Spawns a new actor from the state class T.

Message Processing

T make_response_promise<Ts...>()

Allows an actor to delay its response message.

T response(xs...)

Convenience function for creating fulfilled promises.

Class scheduled_actor

All scheduled actors inherit from scheduled_actor. This includes statically and dynamically typed event-based actors as well as brokers Network I/O with Brokers.

Types

exception_handler

function<error (pointer, std::exception_ptr&)>

default_handler

function<result<message> (pointer, message_view&)>

error_handler

function<void (pointer, error&)>

down_handler

function<void (pointer, down_msg&)>

exit_handler

function<void (pointer, exit_msg&)>

Constructors

(actor_config&)

Constructs the actor using a config.

Termination

quit()

Stops this actor with normal exit reason.

quit(error x)

Stops this actor with error x.

Actor Management

monitor(other, on_down)

Adds a monitor to other (see Monitor).

Special-purpose Handlers

set_exception_handler(F f)

Installs f for converting exceptions to errors (see Errors).

Class blocking_actor

A blocking actor provides a blocking API for message passing. Users generally only interact with blocking actors through a scoped_actor.

Constructors

(actor_config&)

Constructs the actor using a config.

Termination

const error& fail_state()

Returns the current exit reason.

fail_state(error x)

Sets the current exit reason.

Actor Management

monitor(other)

Adds a monitor to other (see Monitor).

demonitor(other)

Removes a monitor from whom.

wait_for(Ts... xs)

Blocks until all actors xs... are done.

await_all_other_actors_done()

Blocks until all other actors are done.

Message Handling

receive(Ts... xs)

Receives a message using the callbacks xs....

receive_for(T& begin, T end)

See receive-loop.

receive_while(F stmt)

See receive-loop.

do_receive(Ts... xs)

See receive-loop.

Messaging Interfaces

Statically typed actors require abstract messaging interfaces to allow the compiler to type-check actor communication. Interfaces in CAF are defined using the variadic template typed_actor<...>, which defines the proper actor handle at the same time. Each template parameter defines one input/output pair via function signature syntax with the return type wrapped in a result. For example, typed_actor<result<string, string>(double)>. Also, the arguments must not use any cv-qualifiers.

In the same way functions cannot be overloaded only by their return type, interfaces cannot accept one input twice (possibly mapping it to different outputs). The example below defines a messaging interface for a simple calculator.

struct calculator_trait {
  using signatures = type_list<result<int32_t>(add_atom, int32_t, int32_t),
                               result<int32_t>(sub_atom, int32_t, int32_t)>;
};

using calculator_actor = typed_actor<calculator_trait>;

It is not required to create a type alias such as calculator_actor, but it makes dealing with statically typed actors much easier. Also, a central alias definition eases refactoring later on.

Interfaces have set semantics. This means the following two type aliases i1 and i2 are considered equal by CAF:

using i1 = typed_actor<replies_to<A>::with<B>, replies_to<C>::with<D>>;
using i2 = typed_actor<replies_to<C>::with<D>, replies_to<A>::with<B>>;

Further, actor handles of type A are assignable to handles of type B as long as B is a subset of A.

For convenience, the class typed_actor<...> defines the member types shown below to grant access to derived types.

Types

behavior_type

A statically typed set of message handlers.

base

Base type for actors, i.e., typed_event_based_actor<...>.

pointer

A pointer of type base*.

stateful_impl<T>

See stateful-actor.

stateful_pointer<T>

A pointer of type stateful_impl<T>*.

extend<Ts...>

Extend this typed actor with Ts....

extend_with<Other>

Extend this typed actor with all cases from Other.

Spawning Actors

Both statically and dynamically typed actors are spawned from an actor_system using the member function spawn. The function takes a function (or function object) as first argument, followed by any number of arguments to pass to the function. The return value of spawn is a handle to the newly spawned actor.

When spawning an actor from a state class State, users can pass the function object caf::actor_from_state<State> to spawn to have CAF automatically pick the correct actor type for the state class and initialize the actor by calling State::make_behavior() (this member function is required).

behavior calculator_fun();
calculator_actor::behavior_type typed_calculator_fun();
struct calculator_state;
struct typed_calculator_state;

Spawning an actor for each implementation is illustrated below.

  auto a1 = sys.spawn(calculator_fun);
  auto a2 = sys.spawn(typed_calculator_fun);
  auto a3 = sys.spawn(actor_from_state<calculator_state>);
  auto a4 = sys.spawn(actor_from_state<typed_calculator_state>);

Additional arguments to spawn are passed to the constructor of a class or used as additional function arguments, respectively. In the example above, none of the functions takes any argument other than the implicit but optional self pointer.

Function-based Actors

When using a function or function object to implement an actor, the first argument can be used to capture a pointer to the actor itself. The type of this pointer is usually event_based_actor* or blocking_actor*. The proper pointer type for any typed_actor handle T can be obtained via T::pointer interface.

Blocking actors simply implement their behavior in the function body. The actor is done once it returns from that function.

Event-based actors can either return a behavior (see Message Handlers) that is used to initialize the actor or explicitly set the initial behavior by calling self->become(...). Due to the asynchronous, event-based nature of this kind of actor, the function usually returns immediately after setting a behavior (message handler) for the next incoming message. Hence, variables on the stack will be out of scope once a message arrives. Managing state in function-based actors can be done either via rebinding state with become, using heap-located data referenced via std::shared_ptr or by using the stateful actor abstraction (see Attaching Cleanup Code to Actors).

The following three functions implement the prototypes shown in spawn and illustrate one blocking actor and two event-based actors (statically and dynamically typed).

// function-based, dynamically typed
behavior calculator_fun() {
  return {
    [](add_atom, int32_t a, int32_t b) { return a + b; },
    [](sub_atom, int32_t a, int32_t b) { return a - b; },
  };
}

// function-based, statically typed
calculator_actor::behavior_type typed_calculator_fun() {
  return {
    [](add_atom, int32_t a, int32_t b) { return a + b; },
    [](sub_atom, int32_t a, int32_t b) { return a - b; },
  };
}

State-based Actors

Implementing an actor using a state class only requires implementing a member function called make_behavior that returns either a behavior or a typed_behavior<...>. Call will automatically deduce the type of the actor from the return type, i.e., returning behavior creates a dynamically typed actor, whereas returning typed_behavior<...> creates a statically typed actor.

The constructor of the class can take any number of arguments. Optionally, the first argument may be a pointer to the actor itself. The type of this pointer must match the return type of make_behavior and is either event_based_actor* or typed_event_based_actor<...>*.

When using typed actors, the usual pattern is to define an alias T for the actor handle and then use T::behavior_type as return type for make_behavior and T::pointer for the self pointer.

The following example shows the implementation of the prototypes shown in spawn by delegating to the function-based implementations we have seen before:

// state-based, dynamically typed
struct calculator_state {
  behavior make_behavior() {
    return {
      [](add_atom, int32_t a, int32_t b) { return a + b; },
      [](sub_atom, int32_t a, int32_t b) { return a - b; },
    };
  }
};

// state-based, statically typed
struct typed_calculator_state {
  calculator_actor::behavior_type make_behavior() {
    return {
      [](add_atom, int32_t a, int32_t b) { return a + b; },
      [](sub_atom, int32_t a, int32_t b) { return a - b; },
    };
  }
};

Attaching Cleanup Code to Actors

Users can attach cleanup code to actors. This code is executed immediately if the actor has already exited. Otherwise, the actor will execute it as part of its termination. The following example attaches a function object to actors for printing a custom string on exit.

// Utility function to print an exit message with custom name.
void print_on_exit(const actor& hdl, const std::string& name) {
  hdl->attach_functor([=](const error& reason) {
    cout << name << " exited: " << to_string(reason) << endl;
  });
}

It is possible to attach code to remote actors. However, the cleanup code will run on the local machine.

Blocking Actors

Blocking actors always run in a separate thread and are not scheduled by CAF. Unlike event-based actors, blocking actors have explicit, blocking receive functions. Further, blocking actors do not handle system messages automatically via special-purpose callbacks. This gives users full control over the behavior of blocking actors. However, blocking actors still should follow conventions of the actor system. For example, actors should unconditionally terminate after receiving an exit_msg with reason exit_reason::kill.

Receiving Messages

The function receive sequentially iterates over all elements in the mailbox beginning with the first. It takes a message handler that is applied to the elements in the mailbox until an element was matched by the handler. An actor calling receive is blocked until it successfully dequeued a message from its mailbox or an optional timeout occurs. Messages that are not matched by the behavior are automatically skipped and remain in the mailbox.

self->receive (
  [](int x) { /* ... */ }
);

Catch-all Receive Statements

Blocking actors can use catch-all callbacks in the same way event-based actors can. A catch-all case simply matches on caf::message and always should be the last callback in the list (because it will render any callback after it unreachable), as shown in the example below.

self->receive(
  [&](float x) {
    // ...
  },
  [&](const down_msg& x) {
    // ...
  },
  [&](const exit_msg& x) {
    // ...
  },
  [](message x) {
    // report unexpected message back to client
    return caf::make_error(caf::sec::unexpected_message);
  }
);

Receive Loops

Message handler passed to receive are temporary object at runtime. Hence, calling receive inside a loop creates an unnecessary amount of short-lived objects. CAF provides predefined receive loops to allow for more efficient code.

// BAD
std::vector<int> results;
for (size_t i = 0; i < 10; ++i)
  receive (
    [&](int value) {
      results.push_back(value);
    }
  );

// GOOD
std::vector<int> results;
size_t i = 0;
receive_for(i, 10) (
  [&](int value) {
    results.push_back(value);
  }
);
// BAD
size_t received = 0;
while (received < 10) {
  receive (
    [&](int) {
      ++received;
    }
  );
} ;

// GOOD
size_t received = 0;
receive_while([&] { return received < 10; }) (
  [&](int) {
    ++received;
  }
);
// BAD
size_t received = 0;
do {
  receive (
    [&](int) {
      ++received;
    }
  );
} while (received < 10);

// GOOD
size_t received = 0;
do_receive (
  [&](int) {
    ++received;
  }
).until([&] { return received >= 10; });

The examples above illustrate the correct usage of the three loops receive_for, receive_while and do_receive(...).until. It is possible to nest receives and receive loops.

bool running = true;
self->receive_while([&] { return running; }) (
  [&](int value1) {
    self->receive (
      [&](float value2) {
        self->println("{} => {}", value1, value2);
      }
    );
  },
  // ...
);

Scoped Actors

The class scoped_actor offers a simple way of communicating with CAF actors from non-actor contexts. It overloads operator-> to return a blocking_actor*. Hence, it behaves like the implicit self pointer in functor-based actors, only that it ceases to exist at scope end.

void test(actor_system& system) {
  scoped_actor self{system};
  // spawn some actor
  auto aut = self->spawn(my_actor_impl);
  self->send(aut, "hi there");
  // self will be destroyed automatically here; any
  // actor monitoring it will receive down messages etc.
}