Actor Models (C++ Concurrency & Lock-Free)

Learning Goal: Designing and implementing a custom actor-model concurrency framework from scratch in C++ to understand lock-free message queues, thread-pool scheduling, and supervisor hierarchies.

Prerequisites

  • Proficient C++: Comfort with pointers, templates, RAII, move semantics, and standard library containers (C++11 or newer).
  • Basic Computer Systems Knowledge: Understanding of memory hierarchies, caches, registers, and basic OS concepts like context switching and processes.

Estimated Study Time

45 Hours (including video lectures, theoretical analysis, and hands-on implementation phases).


Module 1: C++ Multi-Threading Foundations

This module introduces operating system thread management, C++ thread control flows, and basic synchronization primitives. You will learn to construct, manage, and synchronize execution flows while avoiding data races and resource leaks, utilizing modern C++20 patterns like standard cooperative cancellation.

Recommended Videos

  • Why this video is valuable: This video provides a gentle, practical introduction to basic C++ multi-threading. It introduces how std::thread matches hardware threads and executes callables asynchronously, acting as the starting point for your concurrency journey.
  • Knowledge Checkpoint:
    • Spin up a simple asynchronous worker using std::thread.
    • Understand why calling .join() or .detach() is mandatory before a std::thread object goes out of scope.
    • Pass parameters safely by value and reference (std::ref) into a thread function.

  • Why this video is valuable: Anthony Williams (author of C++ Concurrency in Action) covers modern C++20 synchronization mechanics. This is crucial for modernizing our actor framework's core runtime engine with std::jthread (cooperative cancellation), latches (std::latch), and barriers (std::barrier).
  • Knowledge Checkpoint:
    • Implement cooperative thread cancellation using std::stop_token and std::jthread.
    • Distinguish when to use a single-use std::latch versus a reusable std::barrier for thread synchronization.
    • Explain how C++20 synchronization features help avoid complex custom condition-variable logic.

  • Why this video is valuable: This video isolates mutual exclusion primitives (std::mutex) and demonstrates how unguarded memory access leads to undefined data races. It establishes the "lock-based" baseline we will optimize away later in the course.
  • Knowledge Checkpoint:
    • Identify a data race in shared code and fix it using std::mutex and std::lock_guard.
    • Explain how RAII lock managers (std::unique_lock, std::lock_guard) guarantee safe lock release during exceptions.
    • Formulate a scenario that causes a deadlock and list methods to prevent it (e.g., ordering or std::lock).

Module 2: Thread Pools and Task Scheduling

To build an actor framework, you must avoid the high overhead of spawning OS threads dynamically. This module covers implementing a reusable, highly efficient worker thread pool and a task scheduler using modern C++ features.

Recommended Videos

  • Why this video is valuable: A complete walk-through of a standard, lock-based thread pool in C++. It demonstrates how to manage worker lifetimes and dispatch tasks via a synchronized shared queue. This serves as the initial architectural backbone for executing our actor workloads.
  • Knowledge Checkpoint:
    • Construct a pool of workers that block on a std::condition_variable when idle.
    • Safely push generic callables (using type-erased std::function or custom packages) into a thread-safe task queue.
    • Implement a clean pool termination sequence that handles both running tasks and pending tasks without causing deadlocks.

  • Why this video is valuable: Preshing explores real-world task-scheduling architectures designed for high performance. This helps transition your mindset from a simple centralized lock-based queue to distributed, fine-grained task execution architectures.
  • Knowledge Checkpoint:
    • Explain why a centralized, lock-heavy task queue can become a system bottleneck.
    • Understand the concept of task-stealing or decentralized scheduling across multiple worker queues.
    • Define how to decompose large, monolithic programs into independent, asynchronous task units.

  • Why this video is valuable: This video focuses on std::future, std::promise, and std::packaged_task. These asynchronous primitives let tasks return values safely across thread boundaries, which is essential for implementing request-response patterns (like ask or request) in our actor system.
  • Knowledge Checkpoint:
    • Bind a task using std::packaged_task and dispatch it to your thread pool.
    • Retrieve an asynchronous value safely using std::future::get and handle exceptions thrown in background threads.
    • Contrast std::async's default execution policies with dispatching tasks directly to a custom thread pool.

Module 3: Atomics and Lock-Free Queue Design

Locking primitives degrade throughput under heavy thread contention. This module covers lock-free programming using C++ atomic operations and memory ordering, culminating in designing high-performance Single-Producer Single-Consumer (SPSC) and Multi-Producer Multi-Consumer (MPMC) lock-free queues.

Recommended Videos

  • Why this video is valuable: Fedor Pikus delivers an exceptional, deep-dive lecture on C++ atomics, hardware-level caches, and memory barriers. This presentation equips you with the mental model needed to understand atomic read-modify-write loops and memory ordering.
  • Knowledge Checkpoint:
    • Explain how hardware cache lines, cache coherence (MESI), and memory barriers affect multi-threaded throughput.
    • Understand why std::atomic<T>::is_lock_free() is critical and check compiler assumptions.
    • Build a lock-free spinlock using std::atomic_flag and evaluate its performance against std::mutex.

  • Why this video is valuable: Timur Doumler walks through lock-free structures, demonstrating how to design a high-throughput Single-Producer Single-Consumer (SPSC) queue using a circular ring buffer. This highlights the crucial distinction between SPSC and MPMC architectures.
  • Knowledge Checkpoint:
    • Diagram a lock-free circular SPSC queue utilizing atomic head and tail indexes.
    • Apply std::memory_order_acquire and std::memory_order_release to maximize queue performance without relying on sequential consistency.
    • Explain why a lock-free SPSC queue avoids internal race conditions without requiring CAS (Compare-And-Swap) loops.

  • Why this video is valuable: This video unpacks the complexities of implementing a Multi-Producer Multi-Consumer (MPMC) lock-free queue using a ring buffer with generation-based tracking. This design matches what a multi-sender, multi-threaded actor system needs for its mailboxes.
  • Knowledge Checkpoint:
    • Implement a Compare-And-Swap (CAS) loop using std::atomic::compare_exchange_weak to safely update shared queue indexes.
    • Explain generation-based encoding or versioned index techniques to solve the ABA problem in lock-free ring buffers.
    • Describe the differences in performance, safety, and complexity between SPSC, MPMC, and MPSC (Multi-Producer Single-Consumer) configurations.

Module 4: Designing the Actor Model Architecture

This module covers the core concepts of the Actor Model, showing how isolated state, asynchronous message passing, and share-nothing memory layouts provide high scalability compared to traditional thread-and-lock Object-Oriented patterns.

Recommended Videos

  • Why this video is valuable: A classic discussion featuring Carl Hewitt, the pioneer of the Actor Model. It provides the deep theoretical foundation of the paradigm, focusing on three essential elements: internal processing, local state storage, and asynchronous message communications.
  • Knowledge Checkpoint:
    • Define Hewitt's mathematical axioms of an Actor: what actions can an actor perform upon receiving a message?
    • Explain how isolated, unshareable state eliminates the need for shared synchronization locks in application logic.
    • Explain why message passing in the Actor Model is inherently asynchronous and decoupled in space and time.

  • Why this video is valuable: Kevlin Henney compares passive lock-based objects with active, message-driven actors. This distinction helps you shift your design approach away from traditional, call-and-block OO designs.
  • Knowledge Checkpoint:
    • Differentiate between passive synchronization (e.g., monitor objects with locks) and active concurrent entities (actors).
    • Explain the concept "one damn message after another" and how it guarantees sequential execution inside a single actor's scope.
    • Formulate a thread-safe message dispatch system that avoids deadlocks by converting method calls into asynchronous messages.

  • Why this video is valuable: This video demonstrates Erlang's real-world implementation of the Actor Model, showing how isolated processes, message mailboxes, and share-nothing state form a robust concurrency framework.
  • Knowledge Checkpoint:
    • Understand how mailboxes function as asynchronous processing buffers for incoming actor messages.
    • Describe how the Erlang virtual machine schedules thousands of lightweight, independent processes.
    • Explain why sharing mutable memory across actor boundaries is forbidden in standard actor-model patterns.

Module 5: Building the Actor Framework in C++

Here, you will construct your custom C++ actor framework. This involves building the core abstract Actor class, attaching a lock-free queue as its mailbox, and writing the underlying Dispatcher to bind your actors to your thread pool.

Recommended Videos

  • Why this video is valuable: This video shows a concrete implementation of an Actor base class in C++. It demonstrates how to use runtime polymorphism, base virtual handlers, and customized internal messaging mechanisms.
  • Knowledge Checkpoint:
    • Implement an abstract base Actor class featuring a virtual on_receive message processing handler.
    • Integrate a lock-free queue (acting as the actor's mailbox) inside the Actor instance.
    • Define how an actor schedules itself to run on the thread pool when it transitions from "idle" to "has pending messages."

  • Why this video is valuable: Daniela Engert explains how to build an active, non-blocking execution context where agents run concurrently on a shared dispatcher. This shows how to decouple task generation from execution threads in C++.
  • Knowledge Checkpoint:
    • Construct a global execution dispatcher that coordinates thread execution with actor mailboxes.
    • Write a scheduler loop where worker threads pull active actors, process a batch of their mailbox messages, and release them back to the dispatcher.
    • Explain how to handle actor lifetimes safely when an actor is destroyed while message processing is still active.

Architectural Blueprint for Module 5

Because hands-on tutorials for custom C++ actor systems are rare, use the following blueprint to guide your implementation:

+----------------------------------------------+ | Dispatcher | | (Coordinates Thread Pool & Active Actors) | +----------------------+-----------------------+ | Registers / Pushes Active Actors | v +----------------------------------------------+ | Thread Pool | | [Thread 1] [Thread 2] [Thread ... ] | +----------------------+-----------------------+ | Pulls an Actor and processes its mailbox sequentially | v +---------------------------------------+ | Actor Base | | - Mailbox (Lock-free MPMC Queue) | | - State: Idle / Scheduled / Running | | - on_receive(Message) [Pure Virtual] | +---------------------------------------+
  1. The Message Type: Create a type-erased message class (e.g., using std::any or a custom variant union) so actors can receive different payload types safely.
  2. The Mailbox: Embed an atomic MPMC queue into each actor. When actor A sends a message to actor B, it pushes the payload into B's queue.
  3. The Scheduling State: Use an atomic state flag inside each actor (e.g., enum State { Idle, Scheduled, Running }). If actor B's state is Idle when A pushes a message, change B's state to Scheduled and enqueue B's raw pointer into the Dispatcher's global execution queue.
  4. The Execution Loop: The thread pool's workers fetch scheduled actors from the Dispatcher. The thread sets the actor's state to Running, pops a predefined batch of messages from its mailbox, processes them sequentially by calling the virtual on_receive, and then reverts the state to Idle (unless more messages arrived, in which case it schedules itself again).

Self-Study Search Query: "C++ actor model dispatcher implementation std::any" or "C++ thread pool processing scheduled actors lock-free"


Module 6: Supervisor Hierarchies & Fault Tolerance

This final module focuses on fault-tolerant systems using actor supervisor hierarchies. You will learn how to design parent-child relationships where parents monitor, restart, or safely tear down child processes.

Recommended Videos

  • Why this video is valuable: Joe Armstrong (co-creator of Erlang) presents his foundational philosophy: "Let it crash." He explains how isolating crashes inside individual actors and handling failures in supervisors creates resilient concurrent systems.
  • Knowledge Checkpoint:
    • Explain why traditional exception handling (try-catch) often fails in highly asynchronous, concurrent systems.
    • Describe the "Let it crash" philosophy and why isolating failures to individual actors protects the overall system.
    • Understand why actors must have distinct, isolated lifecycles managed by designated supervisors.

  • Why this video is valuable: Explores Erlang's OTP (Open Telecom Platform) framework, detailing supervision trees, monitoring links, and restart strategies. This provides a blueprint for structuring supervisor hierarchies in our framework.
  • Knowledge Checkpoint:
    • Diagram a hierarchical supervision tree consisting of nested root supervisors, sub-supervisors, and leaf-level worker actors.
    • Contrast Erlang’s restart policies: One-For-One (restart only the failed child) and One-For-All (restart all siblings if one fails).
    • Explain how supervisors distinguish between expected child termination and unexpected runtime errors.

  • Why this video is valuable: This lecture explores OTP supervision internals, focusing on why transient failures are resolved by restarts and how to handle persistent faults without getting stuck in infinite restart loops.
  • Knowledge Checkpoint:
    • Explain how a supervisor uses error thresholds (e.g., maximum restarts within a specific time window) to handle persistent faults.
    • Describe how failures propagate up a supervision hierarchy if a local supervisor cannot recover a child.
    • Translate these supervision strategies into your custom C++ framework.

Architectural Blueprint for Module 6

To implement supervisor hierarchies in C++, follow this structured guide to fill the gap of technical videos:

+-------------------------------------------+ | Supervisor Actor | | - Monitored Children List | | - Restart Strategy: One-For-One / All | | - on_child_failure(ChildPtr, Reason) | +---------------------+---------------------+ | Supervises / Spawns / Restarts | v +-------------------------------------------+ | Worker Actor | | - Supervisor Reference | | - Run context (might throw exception) | +-------------------------------------------+
  1. Parent-Child Links: When an actor spawns another actor, register the child's raw pointer or handle (ActorRef) inside the parent. The parent acts as its supervisor.
  2. Monitoring & Exception Isolation: Wrap the virtual processing run loop of the worker actor in a try-catch block inside your thread pool's task execution wrapper.
  3. Failure Propagation: If a worker actor throws an uncaught exception during message processing:
    • Catch it at the thread pool execution wrapper level.
    • Do not crash the worker thread. Instead, wrap the failure event in a system control message (e.g., ChildFailedMessage) containing the child's identifier and the exception details.
    • Asynchronously route this ChildFailedMessage straight into the parent's mailbox.
  4. Handling Failures in the Supervisor: Inside the parent's on_receive, intercept this control message:
    • For One-For-One: Re-instantiate the failed child actor class, restore its initial state, and re-bind its mailbox.
    • For One-For-All: Send shutdown signals to all other registered sibling actors under this supervisor, wait for confirmation, and restart them all.
    • Track restarts using a counter and a timer. If an actor crashes too frequently (e.g., more than 5 times in 10 seconds), escalate the failure by forwarding the crash report to the supervisor's own parent.

Self-Study Search Query: "C++ supervisor pattern implementation" or "handling child actor crashes in C++ thread pool"


Course Map

Below is the recommended sequence of modules. Solid arrows denote direct dependencies.


Key People Index

  • Anthony Williams: Author of the standard reference book C++ Concurrency in Action. He is a key contributor to C++ standardization for concurrency primitives and is the maintainer of the just::thread C++11 concurrency library.
  • Fedor Pikus: Chief Particle Physicist at Mentor Graphics and a regular speaker at CppCon. He is widely recognized for his clear explanations of low-level hardware interactions, lock-free patterns, and high-performance design.
  • Timur Doumler: Developer advocate at JetBrains, active SG14 (Game Dev & Low Latency) C++ committee member, and specialist in audio and low-latency programming. He is a primary source for real-time lock-free practices.
  • Carl Hewitt: Computer scientist and Professor Emeritus at MIT who formulated the Actor Model in 1973 as a formal model of concurrent computation.
  • Joe Armstrong (1950–2019): Co-creator of the Erlang programming language and principal designer of the Open Telecom Platform (OTP) framework. His work defined modern fault-tolerant concurrent systems architecture.

Final Self-Assessment

Verify your understanding and custom framework implementation against these key requirements:

  • Thread Resource Safety: My thread pool uses C++20 std::jthread and cooperative cancellation tokens to safely spin down and clean up resources without leaving orphaned threads or leaks.
  • No Raw Mutex Locks in Mailboxes: My actors' message mailboxes are implemented using a lock-free Single-Producer Single-Consumer (SPSC) or Multi-Producer Multi-Consumer (MPMC) queue that does not rely on mutex locks.
  • Lock-free Queue Atomicity: My lock-free queues use explicit memory orderings (std::memory_order_release / std::memory_order_acquire) and CAS loops, verified to be data-race-free under sanitizers.
  • State Isolation: No two actor instances share raw pointers to mutable states. All information exchanges occur exclusively via immutable values passed as typed or type-erased messages.
  • Dynamic Task Scheduling: The dispatcher dynamically allocates CPU execution slices on the thread pool to active actors with pending messages, returning them to an idle state once their mailboxes are clear.
  • Exception Containment: The thread pool's task executor intercepts unexpected exceptions in worker actors. It isolates the crash, preventing the worker thread itself from terminating.
  • Parent-Child Supervision Tree: I have implemented parent-child links that let parent actors monitor child lifecycles and receive notification messages if a child fails.
  • Supervisor Restart Policy: My supervisor actors support at least one configurable recovery strategy (such as One-For-One or One-For-All) to restart failed child actors and restore them to a clean state.
  • Escalation & Infinite Loops: My supervisors track child restart frequencies, safely escalating failures up the tree if a child hits its maximum restart threshold within a given time frame.
Explore Further

Related Computer Science Roadmaps

View All