Stop thinking in threads and mutexes. Start thinking in actors and messages. Discover how qb
's implementation of the Actor Model simplifies concurrent programming and eliminates entire classes of bugs.
Target Audience: Beginner / Intermediate C++ developers curious about concurrent programming models.
GitHub: https://github.com/isndev/qb
If you've ever written multi-threaded C++ code, you know the pain of shared state. std::mutex
, std::atomic
, and std::condition_variable
are powerful tools, but they're also a minefield of potential deadlocks, race conditions, and complexity.
The qb
framework offers a more elegant solution: the Actor Model. Instead of sharing memory, actors share nothing. They are completely isolated, stateful entities that communicate only by sending immutable messages to each other. This fundamental design choice makes concurrent programming dramatically simpler and safer.
The Three Pillars of a qb
Actor
Every actor in the qb
framework adheres to three simple rules:
- It has a private state. An actor's member variables can only be touched by the actor itself. This isolation is the cornerstone of its safety.
- It has an address. Every actor has a unique
qb::ActorId
, which other actors use to send it messages. - It processes messages sequentially. Each actor has a "mailbox." It pulls one message at a time from this mailbox and processes it completely before moving to the next. This guarantees that an actor's internal state is never accessed concurrently.
+-----------------+
| SenderActor |
+-----------------+
|
| sends MessageEvent(data)
|
+-------------------+-------------------+
| v |
| +-----------------+ |
| | Mailbox (Queue) | |
| +-----------------+ |
| | MessageEvent | |
| | AnotherEvent | |
| | ... | |
| +-----------------+ |
| | |
| | process one-by-one|
| v |
| +-----------------+ |
| | ReceiverActor | |
| | (private state) | |
| +-----------------+ |
| |
+---------- Core QB Framework ----------+
Request-Response: The Hello World of Actor Communication
One of the most common interaction patterns is request-response. Let's see how this works in qb
with a simple example where "Sender" actors ask a "Receiver" to do some work and reply.
This example demonstrates two key concepts:
- A sender pushing a message to a known destination (
receiver_id
). - A receiver using
event.getSource()
to dynamically discover the sender's address and send a reply.
From examples/core/example2_basic_actors.cpp
#include <qb/actor.h>
#include <qb/main.h>
#include <qb/io.h>
// 1. Define the request and response events
struct MessageEvent : public qb::Event {
std::string content;
MessageEvent(std::string msg) : content(std::move(msg)) {}
};
struct ResponseEvent : public qb::Event {
std::string content;
ResponseEvent(std::string msg) : content(std::move(msg)) {}
};
// 2. The ReceiverActor processes requests and sends back responses
class ReceiverActor : public qb::Actor {
public:
ReceiverActor() {
registerEvent<MessageEvent>(*this);
}
// Process a message and reply to the original sender
void on(MessageEvent& event) { // Note: event is non-const
qb::io::cout() << "Receiver " << id()
<< ": Received '" << event.content << "' from "
<< event.getSource() << "\n";
// To reply, we use the source ID from the incoming event
auto original_sender = event.getSource();
std::string response_msg = "Acknowledged: " + event.content;
// Push a response event back to the sender
push<ResponseEvent>(original_sender, response_msg);
}
};
// 3. The SenderActor sends requests and waits for responses
class SenderActor : public qb::Actor {
private:
qb::ActorId _receiver_id;
public:
SenderActor(qb::ActorId receiver_id) : _receiver_id(receiver_id) {
registerEvent<ResponseEvent>(*this);
}
bool onInit() override {
qb::io::cout() << "Sender " << id() << ": Sending message to receiver "
<< _receiver_id << "\n";
// Send a request to the receiver
push<MessageEvent>(_receiver_id, "Hello from Sender!");
return true;
}
// Handle the response from the receiver
void on(ResponseEvent& event) {
qb::io::cout() << "Sender " << id()
<< ": Received response '" << event.content
<< "' from " << event.getSource() << "\n";
// Task complete, shut down
kill();
}
};
int main() {
qb::Main engine;
// Create the receiver first to get its ID
auto receiver_id = engine.addActor<ReceiverActor>(0);
// Create the sender and give it the receiver's ID
engine.addActor<SenderActor>(0, receiver_id);
engine.start();
engine.join();
qb::io::cout() << "Application finished." << std::endl;
return 0;
}
Why This is a Big Deal
- No Locks, No Race Conditions: Notice the complete absence of
std::mutex
. TheReceiverActor
can be hammered with messages from hundreds of senders on different threads, and its internal state remains safe because theon(MessageEvent&)
handler is guaranteed to execute atomically for each message. - Location Transparency: The sender doesn't know or care if the receiver is on the same core, a different core, or even a different machine (in a distributed
qb
system). Thepush<Event>(destination, ...)
API remains the same. - Decoupled & Testable: Actors are naturally decoupled. You can test
ReceiverActor
in isolation by simply sending itMessageEvent
s and asserting that it produces the correctResponseEvent
s.
By embracing the actor model, qb
provides a powerful yet simple mental framework for building complex, concurrent systems. It lets you focus on your application's logic and flow, not on the low-level mechanics of synchronization.
Ready to dive deeper? Explore the qb
framework and its examples on GitHub.
-
qb
Core Framework: https://github.com/isndev/qb - Official Examples: https://github.com/isndev/qb-examples
Top comments (0)