Actors systems can span complex communication graphs that make it hard to decide when actors are no longer needed. As a result, manually managing lifetime of actors is merely impossible. For this reason, CAF implements a garbage collection strategy for actors based on weak and strong reference counts.
Smart Pointers to Actors¶
In CAF, we use a different approach than the standard library because (1) we always allocate actors along with their control block, (2) we need additional information in the control block, and (3) we can store only a single raw pointer internally instead of the two raw pointers
std::shared_ptr needs. The following figure summarizes the design of smart pointers to actors.
strong_actor_ptr instead of
weak_actor_ptr instead of
std::weak_ptr<...>. Unlike the counterparts from the standard library, both smart pointer types only store a single pointer.
Also, the control block in CAF is not a template and stores the identity of an actor (
node_id). This allows CAF to access this information even after an actor died. The control block fits exactly into a single cache line (64 Bytes). This makes sure no false sharing occurs between an actor and other actors that have references to it. Since the size of the control block is fixed and CAFguarantees the memory layout enforced by
actor_storage, CAF can compute the address of an actor from the pointer to its control block by offsetting it by 64 Bytes. Likewise, an actor can compute the address of its control block.
The smart pointer design in CAF relies on a few assumptions about actor types. Most notably, the actor object is placed 64 Bytes after the control block. This starting address is cast to
T* must be convertible to
reinterpret_cast. In practice, this means actor subclasses must not use virtual inheritance, which is enforced in CAF with a
Strong and Weak References¶
A strong reference manipulates the
strong refs counter as shown above. An actor is destroyed if there are zero strong references to it. If two actors keep strong references to each other via member variable, neither actor can ever be destroyed because they produce a cycle (see § 1.5). Strong references are formed by
typed_actor<...> (see § :ref:`actor-reference <#actor-reference>`__).
A weak reference manipulates the
weak refs counter. This counter keeps track of how many references to the control block exist. The control block is destroyed if there are zero weak references to an actor (which cannot occur before
strong refs reached zero as well). No cycle occurs if two actors keep weak references to each other, because the actor objects themselves can get destroyed independently from their control block. A weak reference is only formed by
actor_addr (see § :ref:`actor-address <#actor-address>`__).
Converting Actor References with
actor_cast converts between actor pointers and handles. The first common use case is to convert a
strong_actor_ptr to either
typed_actor<...> before being able to send messages to an actor. The second common use case is to convert
strong_actor_ptr to upgrade a weak reference to a strong reference. Note that casting
actor_addr to a strong actor pointer or handle can result in invalid handles. The syntax for
actor_cast resembles builtin C++ casts. For example,
x to an handle of type
Breaking Cycles Manually¶
Cycles can occur only when using class-based actors when storing references to other actors via member variable. Stateful actors (see § :ref:`stateful-actor <#stateful-actor>`__) break cycles by destroying the state when an actor terminates, before the destructor of the actor itself runs. This means an actor releases all references to others automatically after calling
quit. However, class-based actors have to break cycles manually, because references to others are not released until the destructor of an actor runs. Two actors storing references to each other via member variable produce a cycle and neither destructor can ever be called.
Class-based actors can break cycles manually by overriding
on_exit() and calling
destroy(x) on each handle (see § :ref:`actor-handle <#actor-handle>`__). Using a handle after destroying it is undefined behavior, but it is safe to assign a new value to the handle.