Understand how QB implements the Actor Model for robust, concurrent programming, focusing on qb::Actor and qb::ActorId.
Core Concept: The Actor Model in QB
The QB framework is built upon the Actor Model, a powerful paradigm for designing concurrent and distributed systems. Instead of directly managing threads and locks, you work with actors: independent, isolated units of computation and state.
qb::Actor: The Heart of Your Concurrent Logic
Every actor in your system will inherit from the qb::Actor base class (qb/core/Actor.h). Think of an actor as a specialized object with these key characteristics:
- Autonomous and Isolated: Each actor is a self-contained entity. Its internal data (member variables, or "state") is strictly private and protected from direct access by any other actor. This is fundamental to preventing data races and simplifying concurrent state management.
- Message-Driven: Actors interact exclusively by sending asynchronous messages (called events) to one another. An actor's behavior is defined by how it reacts to the events it receives.
- Sequential Processing: Each actor has an implicit mailbox where incoming events are queued. The actor processes these events one at a time, in the order they are effectively received by its managing VirtualCore. This sequential processing of its own events guarantees that an actor's internal state is modified safely, without internal race conditions.
A simplified view of actor interaction:
+-----------+ +-----------------+ +-----------+
| Actor A |----->| Event Message |----->| Actor B |
| (Sender) | | (e.g., MyEvent) | | (Receiver)|
+-----------+ +-----------------+ +-----------+
| ^
| 1. push<MyEvent>(actor_b_id, ...) | 3. Processes MyEvent
+-------------------------------------------+
| 2. Event arrives in
| Actor B's mailbox
Defining a Simple Actor
Let's look at a basic example. (A runnable version can be found in example/core/example1_simple_actor.cpp.)
#include <iostream>
int increment_by;
explicit CountEvent(int amount) : increment_by(amount) {}
};
private:
int _current_count = 0;
public:
CounterActor() = default;
return true;
}
void on(
const CountEvent& event) {
_current_count += event.increment_by;
qb::io::cout() <<
"CounterActor [" <<
id() <<
"] received CountEvent. Count is now: " << _current_count <<
".\n";
if (_current_count >= 10) {
qb::io::cout() <<
"CounterActor [" <<
id() <<
"] reached target. Terminating.\n";
}
}
void on(
const qb::KillEvent& ) {
qb::io::cout() <<
"CounterActor [" <<
id() <<
"] received KillEvent. Shutting down.\n";
}
~CounterActor() override {
qb::io::cout() <<
"CounterActor [" <<
id() <<
"] destroyed. Final count: " << _current_count <<
".\n";
}
};
Convenience header for the core QB Actor components.
Convenience header for the QB Event system.
Base class for all actors in the qb framework.
Definition Actor.h:106
void registerEvent(_Actor &actor) const noexcept
Subscribe this actor to listen for a specific event type.
void on(KillEvent const &event) noexcept
Handler for KillEvent.
ActorId id() const noexcept
Get ActorId.
Definition Actor.h:368
CoreId getIndex() const noexcept
Get core index.
void kill() const noexcept
Terminate this actor and mark it for removal from the system.
virtual bool onInit()
Initialization callback, called once after construction and ID assignment.
Definition Actor.h:198
Base class for all events in the actor system.
Definition Event.h:85
Thread-safe console output class.
Definition io.h:100
Core I/O and logging utilities for the qb framework.
Key takeaways from the example:
- onInit() is Vital: This is where you must call registerEvent<YourEventType>(*this) for every type of event your actor intends to process.
- Event Handlers: Public methods named on with a parameter of the event type (e.g., void on(const CountEvent& event)).
- Self-Termination: An actor can decide to shut itself down by calling kill().
- RAII: Use member variables with destructors (like std::unique_ptr, std::fstream, custom classes) for resource management. They will be cleaned up when the actor is destroyed.
Every active actor in the QB system possesses a unique qb::ActorId (qb/core/ActorId.h). This ID is the "address" you use to send events to a specific actor.
- Composition: It's a 32-bit identifier combining:
- CoreId (uint16_t): The index of the VirtualCore (worker thread) hosting the actor.
- ServiceId (uint16_t): An ID unique within that specific core.
- How to Get It:
- Inside an actor: Call the id() method.
- When creating an actor: Returned by main.addActor<MyActor>(core_id, ...), core.addActor<MyActor>(...), or parent_actor.addRefActor<ChildActor>()->id().
- Key qb::ActorId Methods:
- sid(): Returns the ServiceId part.
- index(): Returns the CoreId part.
- is_valid(): Checks if the ID is a valid, assigned ID (not the default-constructed, invalid ActorId()).
- is_broadcast(): Checks if it's a special broadcast ID.
- Special ActorId Values:
- qb::ActorId(): A default-constructed, invalid ID. Useful for uninitialized ID members.
- qb::BroadcastId(core_id): A special ID used with push<Event>(BroadcastId(core_id), ...) to send an event to all actors running on the specified core_id. The underlying ServiceId for this is ActorId::BroadcastSid.
push<MyEvent>(some_target_actor_id, );
}
push<SystemUpdateEvent>(core1_broadcast_target, );
Unique identifier for actors.
Definition ActorId.h:374
bool is_valid() const noexcept
Check if this ActorId is valid (not NotFound)
Specialized ActorId for broadcasting messages to all actors on a core.
Definition ActorId.h:445
Actor Lifecycle: A Brief Overview
The journey of an actor involves several stages, from creation to destruction. The onInit() and ~Actor() methods are critical hooks into this lifecycle.
(For a detailed step-by-step breakdown, see: QB-Core: Actor Lifecycle**)**
Managing Actor State
- Encapsulation is Key: Always keep an actor's state (its member variables) private or protected.
- Sequential Access = Thread Safety: Because a VirtualCore processes events for an actor one by one, you don't need mutexes or other locks to protect the actor's own member variables from race conditions caused by its own event handlers.
- Avoid Blocking: Event handlers (on(Event&) methods) and ICallback::onCallback() implementations must never block. Long computations, synchronous I/O (like reading a file directly in a handler), or waiting on external locks will freeze the entire VirtualCore, preventing it from processing events for any actor assigned to it. Offload such tasks using qb::io::async::callback or by delegating to other specialized actors.
- Interacting with External Shared State: If an actor must interact with resources shared outside the actor model (e.g., a global static variable, a non-thread-safe third-party library), you are responsible for ensuring thread-safe access to that external resource. Consider encapsulating such resources within a dedicated "manager" actor to serialize access.
By adhering to these principles, you can build complex, concurrent applications where state management is significantly simplified and common concurrency bugs are inherently avoided.
(Next: Core Concepts: QB Event System**)**