DEV Community

ISNDEV
ISNDEV

Posted on

Goodbye, Mutexes: An Introduction to the C++ Actor Model with `qb`

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) {}
};
Enter fullscreen mode Exit fullscreen mode

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.
    }
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (0)