Description: Feeling the pain of multithreading with locks and condition variables? Discover the Actor Model, a powerful paradigm for writing concurrent C++ that is simpler, safer, and more scalable. We'll build a "Ping-Pong" application from scratch using the qb
framework to see how it works in practice.
Target Audience: Beginner / Intermediate C++ developers curious about modern concurrency.
The Agony of Traditional Threading
For decades, concurrent C++ has been synonymous with std::thread
, std::mutex
, and std::condition_variable
. While powerful, these tools force you to manually manage shared state, leading to a minefield of potential issues:
- Race Conditions: When multiple threads access shared data and at least one access is a write, leading to unpredictable results.
- Deadlocks: Two or more threads waiting for each other to release a lock, freezing your application forever.
- Complexity: Reasoning about all possible interleavings of thread execution becomes exponentially harder as your application grows.
There has to be a better way.
Introducing the Actor Model
The Actor Model offers a fundamentally different approach to concurrency. Instead of sharing memory, your application is composed of isolated, independent "actors."
- Isolation: An actor maintains its own private state that no other actor can touch directly.
- Message-Driven: Actors communicate exclusively by sending asynchronous, immutable messages (or "events") to each other.
- Mailbox: Each actor has a "mailbox" where incoming messages are queued. It processes these messages one at a time, sequentially.
This sequential, single-threaded processing within each actor eliminates data races on its internal state by design. The complexity shifts from low-level locking to high-level message flow.
Meet the qb
Framework: Your C++17 Toolkit for Concurrency
The qb Framework is a modern C++17 library designed from the ground up around the Actor Model. It provides the tools to build high-performance, scalable, and robust concurrent systems without the manual pain of thread and mutex management.
Building Our First Actors: Ping & Pong
Let's build the "Hello, World!" of actor systems. One actor, PingerActor
, will send a PingEvent
to another, PongerActor
, which will simply print a message.
First, we need to define the message itself. In qb
, these are called events.
Events: The Language of Actors
An event is just a data structure that inherits from qb::Event
.
// An event to signal the Ponger.
struct PingEvent : qb::Event {
// We can add any data we need to send.
int ping_number;
// A constructor makes creating events easy.
PingEvent(int number)
: ping_number(number) {}
};
The Actors
Now, let's define our PongerActor
. It waits for a PingEvent
and prints a message.
#include <qb/actor.h>
#include <qb/event.h>
#include <qb/io.h> // For thread-safe cout
class PongerActor : public qb::Actor {
public:
// This is called once after the actor is created.
bool onInit() override {
qb::io::cout() << "PongerActor is ready to receive pings!\n";
// CRUCIAL: We must register which events we want to handle.
registerEvent<PingEvent>(*this);
return true;
}
// This method is the handler for PingEvent.
// The framework calls this automatically when a PingEvent arrives.
void on(const PingEvent& event) {
qb::io::cout() << "Ponger received Ping #" << event.ping_number
<< " from actor " << event.getSource() << ".\n";
// In a real app, we would push a PongEvent back.
// For this simple example, we'll stop here.
}
};
The qb::Main
Engine: Bringing It All to Life
Actors don't run on their own. They are managed by the qb::Main
engine, which orchestrates their creation, scheduling, and message delivery.
#include <qb/main.h>
// Include our actor definitions...
int main() {
qb::io::cout() << "--- Starting Ping-Pong Example ---\n";
// 1. Create the engine.
qb::Main engine;
// 2. Add actors to a virtual core (a worker thread).
// Let's create the Ponger first on core 0.
auto ponger_id = engine.addActor<PongerActor>(0);
// Now, create the Pinger. We need to pass the Ponger's ID to its constructor.
// In a full app, you would define PingerActor similarly to PongerActor.
// auto pinger_id = engine.addActor<PingerActor>(0, ponger_id);
// 3. Start the engine.
qb::io::cout() << "Engine starting...\n";
// `start(false)` runs the engine in the current thread and blocks until it's done.
engine.start(false);
qb::io::cout() << "Engine stopped.\n";
return 0;
}
This simple structure already gives us enormous benefits. We wrote concurrent logic without a single std::mutex
or std::thread
. The PongerActor
can process PingEvent
messages safely, and if we had thousands of such actors, the qb
engine could efficiently schedule them across multiple CPU cores.
Conclusion: A New Way to Think About C++ Concurrency
The Actor Model, powered by the qb
framework, isn't just another library; it's a paradigm shift. It encourages you to design systems as collections of independent, message-passing components, leading to code that is:
- Safer: Eliminates entire classes of concurrency bugs by design.
- Simpler: Easier to reason about than complex lock-based code.
- Scalable: Naturally maps to multi-core architectures.
This was just a small taste. In future articles, we'll explore how qb
and its modules (qbm
) make it easy to build high-performance web servers, database clients, and real-time WebSocket applications with the same elegant, actor-based principles.
Find out more:
-
qb
Framework on GitHub: https://github.com/isndev/qb -
qb-examples
Repository: https://github.com/isndev/qb-examples
Top comments (0)