Project 15: Actor Model Framework
Quick Reference
| Attribute | Value |
|---|---|
| Project Number | 15 of 17 |
| Difficulty | Master |
| Time Estimate | 4-6 weeks |
| Language | C++ (C++17 minimum, C++20 recommended) |
| Alternative Languages | Erlang (for comparison), Rust, Scala |
| Prerequisites | Projects 3 (Thread Pool) and 6 (Lock-Free Queue) completed |
| Knowledge Area | Concurrency Patterns / Message Passing |
| Key Concepts | Actor isolation, mailboxes, supervision trees, message dispatch, location transparency |
| Main Book | “C++ Concurrency in Action, Second Edition” by Anthony Williams |
| Secondary Reference | “Reactive Design Patterns” by Roland Kuhn |
| Coolness Level | Level 5: Pure Magic (Super Cool) |
| Business Potential | Open Core Infrastructure |
Learning Objectives
By completing this project, you will:
- Master the Actor Concurrency Model: Understand how isolating state within actors eliminates data races by design
- Build Lock-Free Mailboxes: Implement per-actor message queues using lock-free techniques from Project 6
- Design a Message Dispatch System: Create type-safe message handling using
std::variantorstd::anywith pattern matching - Integrate with Thread Pool Schedulers: Execute actors efficiently on your work-stealing thread pool from Project 3
- Implement Supervision Hierarchies: Build fault-tolerant systems where parent actors manage child lifecycle and failures
- Create Ask/Tell Communication Patterns: Implement both fire-and-forget and request-reply messaging with futures
- Understand Location Transparency: Abstract away whether actors run locally or remotely (foundation for distributed systems)
- Build Reactive Systems: Learn how message-driven architectures enable responsive, resilient, and elastic systems
Theoretical Foundation
Core Concepts
The Actor Model: A Different Way to Think About Concurrency
The Actor Model, introduced by Carl Hewitt in 1973, flips the traditional approach to concurrent programming on its head:
Traditional Concurrency (Shared State):
Thread 1 Thread 2 Thread 3
| | |
v v v
+--------------------------------------+
| SHARED MEMORY |
| +---------+ +---------+ |
| | Counter | | Queue | ...... |
| +---------+ +---------+ |
+--------------------------------------+
| |
v v
[MUTEX] [MUTEX] [MUTEX] ...
Problems:
- Deadlocks (lock ordering)
- Data races (forgotten locks)
- Complex reasoning (any thread can touch any data)
Actor Model (Message Passing):
+----------+ +----------+ +----------+
| Actor A | | Actor B | | Actor C |
| | | | | |
| [state] | | [state] | | [state] |
| [mailbox]|<--msg--| |--msg-->| [mailbox]|
| | | [mailbox]|<--msg--| |
+----------+ +----------+ +----------+
| | |
v v v
Process messages one at a time (no concurrent access!)
Benefits:
- No shared mutable state between actors
- No locks needed (messages are copied/moved)
- Each actor processes one message at a time
- Easy to reason about (sequential within actor)
Key insight: In the actor model, concurrency exists between actors (they run in parallel), but within each actor, execution is sequential. This eliminates the need for synchronization primitives like mutexes within actor logic.
The Three Axioms of Actors
When an actor receives a message, it can do exactly three things:
- Send messages to other actors it knows about (including itself)
- Create new actors (becoming their supervisor)
- Designate behavior for the next message (change internal state)
Message Received by Actor:
+------------------+
| Incoming Message |
+--------+---------+
|
v
+------------------+
| Actor Logic |
+--------+---------+
|
+--------+---------+---------+
| | | |
v v v v
Send Create Change Reply
Messages Actors State (optional)
These three axioms are complete. Any concurrent computation can be expressed using only these operations.
Actor Lifecycle and Mailboxes
Every actor has:
- Identity (ActorRef): A handle for sending messages
- State: Private data that only this actor can access
- Behavior: Code that processes incoming messages
- Mailbox: A queue of pending messages
Actor Lifecycle:
Created
|
v
+---------+ receive() +-----------+
| Idle |--------------->| Processing |
| (in |<---------------| Message |
| mailbox)| done +-----------+
+---------+ |
| | error
| stop() v
v +-------------+
+---------+ | Failed |
| Stopped | | (notify |
+---------+ | supervisor) |
+-------------+
|
v
Supervisor decision:
- Restart
- Stop
- Escalate
Message Types and Dispatch
Messages in an actor system need type erasure since a mailbox must hold arbitrary message types:
Message Dispatch Options:
Option 1: std::variant (compile-time, type-safe)
using Message = std::variant<
Increment,
GetCount,
Reset,
Shutdown
>;
void receive(const Message& msg) {
std::visit(overloaded{
[this](const Increment& m) { count += m.amount; },
[this](const GetCount& m) { reply(Count{count}); },
[this](const Reset&) { count = 0; },
[this](const Shutdown&) { stop(); }
}, msg);
}
Pros: Fast dispatch, exhaustive matching
Cons: All message types must be known at compile time
Option 2: std::any (runtime, flexible)
void receive(const std::any& msg) {
if (auto* inc = std::any_cast<Increment>(&msg)) {
count += inc->amount;
} else if (auto* get = std::any_cast<GetCount>(&msg)) {
reply(Count{count});
}
// ...
}
Pros: Add message types at runtime
Cons: Slower dispatch, no compile-time checking
Option 3: Virtual base class
struct Message { virtual ~Message() = default; };
struct Increment : Message { int amount; };
void receive(std::unique_ptr<Message> msg) {
if (auto* inc = dynamic_cast<Increment*>(msg.get())) {
count += inc->amount;
}
// ...
}
Pros: Familiar OOP pattern
Cons: Heap allocation per message, RTTI overhead
Supervision Trees: Fault Tolerance by Design
In Erlang/OTP (and Akka), actors are organized into supervision trees. When a child actor fails, its parent (supervisor) decides what to do:
Supervision Hierarchy:
+------------------+
| /user | (system root)
| (UserGuardian) |
+--------+---------+
|
+----------------+----------------+
| | |
+-------v------+ +------v-------+ +-----v--------+
| /user/app | | /user/db | | /user/web |
| (AppSupervisor)| | (DBSupervisor)| | (WebSupervisor)|
+-------+------+ +------+-------+ +------+-------+
| | |
+-------+-------+ +----+----+ +-----+-----+
| | | | | | |
Actor Actor Actor Conn1 Conn2 Handler1 Handler2
A B C Pool Pool Worker Worker
Supervision Strategies:
1. One-for-One:
Child fails --> Only that child is restarted
2. One-for-All:
Child fails --> All children are restarted
3. Rest-for-One:
Child fails --> That child and all children after it are restarted
Why supervision matters: Traditional exception handling assumes the caller can fix the problem. But in concurrent systems, errors often occur in components you don’t directly call. Supervision provides a structured way to handle these failures.
Ask vs Tell Patterns
Two fundamental communication patterns exist between actors:
Tell Pattern (Fire and Forget):
actorRef.tell(MyMessage{data}); // Returns immediately
// No response expected
// Sender continues without waiting
Use when:
- Commands that don't need confirmation
- Events being broadcast
- High-throughput scenarios
Ask Pattern (Request-Reply):
std::future<Response> future = actorRef.ask<Response>(Query{});
// Returns a future immediately
// Can await the response later
Response response = future.get(); // Blocks until response
Use when:
- Need response to continue
- Implementing synchronous APIs over actors
- Gathering data from multiple actors
Implementation of Ask:
+----------+ +----------+
| Sender | | Receiver |
+----------+ +----------+
| |
| 1. Create temp actor |
| 2. Send message with |
| replyTo = temp actor |
|------------------------------>|
| |
| 3. Process message |
| |
| 4. Send reply to replyTo |
|<------------------------------|
| |
| 5. Temp actor receives, |
| fulfills promise |
| |
v v
future.get() returns with response
Why This Matters
Industry Adoption: The actor model powers some of the most demanding systems:
| Company | System | Scale |
|---|---|---|
| Erlang-based messaging | 2 billion users, 65 billion messages/day | |
| Akka for feed processing | 500M members, real-time updates | |
| Discord | Elixir/Erlang infrastructure | 19M concurrent users peak |
| Microsoft Orleans | .NET Virtual Actors | Xbox Cloud Gaming, Halo |
| Finagle (actor-like) | 500M tweets/day |
Career Impact: Understanding actors prepares you for:
- Distributed systems design (actors naturally span machines)
- Game engine development (Unity’s DOTS is actor-inspired)
- Financial systems (event sourcing + actors)
- IoT and embedded systems (message-driven is natural for hardware)
Mental Model Benefits: Even when not using actors directly, thinking in terms of isolated components communicating via messages improves:
- Microservices design
- Event-driven architectures
- Reactive programming patterns
Historical Context
1973: Carl Hewitt publishes “A Universal Modular ACTOR Formalism for Artificial Intelligence” at MIT, introducing the actor model as a mathematical theory of computation.
1986: Erlang is developed at Ericsson for telephone switches. It implements actors (called “processes”) with supervision trees. Erlang systems achieve “nine nines” reliability (99.9999999% uptime).
2009: Akka framework brings actors to the JVM (Scala/Java), heavily inspired by Erlang/OTP.
2012: Microsoft introduces Orleans, a “Virtual Actor” model that automatically manages actor lifecycle and distribution.
2015: Elixir gains popularity, bringing modern syntax to the Erlang VM with full actor support.
2020s: Actors influence modern patterns:
- Swift Actors (language-level support in Swift 5.5)
- Rust’s actix framework
- C++ proposals for executor-based actors
Common Misconceptions
| Misconception | Reality |
|---|---|
| “Actors are just threads” | Actors are logical entities scheduled on thread pools. Millions of actors can run on a handful of threads. |
| “Actors are slow because of message copying” | Move semantics eliminate most copies. Lock-free mailboxes provide sub-microsecond latency. |
| “You need Erlang/Scala for actors” | Actors are a pattern, not a language feature. C++ implementations can be very efficient. |
| “Actor systems are always distributed” | Actors work great for single-machine concurrency. Distribution is an extension. |
| “Supervision means automatic error recovery” | Supervision provides structure for recovery. You still design the recovery strategy. |
| “Actors eliminate all concurrency bugs” | Actors eliminate data races but not logical race conditions (wrong message order, timeout bugs). |
Project Specification
What You Will Build
A complete actor framework for C++ with the following components:
- Actor Base Class: Foundation for user-defined actors with message handling
- ActorRef: Type-erased handle for sending messages to actors
- Mailbox: Lock-free per-actor message queue using techniques from Project 6
- ActorSystem: Container managing actor lifecycle and scheduling on thread pool
- Supervision: Parent-child relationships with restart policies
- Ask/Tell Patterns: Fire-and-forget and request-reply messaging
Functional Requirements
Core Actor Operations
// Define a custom actor
class CounterActor : public Actor {
int count = 0;
public:
// Message handlers
void receive(const Increment& msg) {
count += msg.amount;
}
void receive(const GetCount& msg) {
reply(Count{count});
}
void receive(const Reset&) {
count = 0;
}
};
// Create actor system
ActorSystem system(8); // 8 worker threads
// Spawn actors
auto counter = system.spawn<CounterActor>("counter");
// Tell (fire and forget)
counter.tell(Increment{5});
counter.tell(Increment{3});
// Ask (request-reply)
auto future = counter.ask<Count>(GetCount{});
Count result = future.get();
assert(result.value == 8);
Supervision
class WorkerSupervisor : public Actor {
public:
void preStart() override {
// Spawn children with supervision
for (int i = 0; i < 4; ++i) {
auto worker = spawn<WorkerActor>("worker-" + std::to_string(i));
supervise(worker, SupervisionStrategy::Restart);
}
}
void onChildFailed(ActorRef child, std::exception_ptr error) override {
log("Child {} failed, applying supervision strategy", child.path());
// Default behavior: apply configured strategy
}
};
Non-Functional Requirements
| Requirement | Target |
|---|---|
| Message Throughput | > 1M messages/second (single actor receiving) |
| Actor Creation | > 100K actors/second |
| Ask Latency | < 10us average round-trip |
| Memory per Actor | < 1KB (excluding user state) |
| Scalability | Linear with cores up to 16 threads |
| Thread Safety | Zero data races (TSan clean) |
Example Usage / Output
// Example: Ping-Pong Actors
struct Ping { int count; ActorRef sender; };
struct Pong { int count; };
class PingActor : public Actor {
public:
void receive(const Pong& msg) {
if (msg.count > 0) {
ponger.tell(Ping{msg.count - 1, self()});
} else {
system().shutdown();
}
}
void setTarget(ActorRef target) { ponger = target; }
void start(int n) { ponger.tell(Ping{n, self()}); }
private:
ActorRef ponger;
};
class PongActor : public Actor {
public:
void receive(const Ping& msg) {
msg.sender.tell(Pong{msg.count});
}
};
Expected Output:
$ ./actor_demo
Actor System starting with 8 workers...
[System] Spawned SupervisorActor@/user/supervisor
[SupervisorActor] Spawning CounterActor
[SupervisorActor] Spawning WorkerActor
[System] Actor hierarchy:
/user
/user/supervisor
/user/supervisor/counter
/user/supervisor/worker
[WorkerActor] Processing jobs...
[CounterActor] Increment received, count=1
[CounterActor] Increment received, count=2
[WorkerActor] Error: simulated failure!
[SupervisorActor] Child WorkerActor failed, restarting...
[WorkerActor] Restarted, resuming work...
[CounterActor] GetCount query, responding with 2
Final count: 2
System Statistics:
Actors created: 3
Actors currently active: 3
Messages sent: 1,247
Messages processed: 1,247
Average message latency: 1.2us
System uptime: 5.0s
$ ./actor_benchmark
Benchmark: Message Throughput
Single producer, single consumer actor
10,000,000 messages
Results:
Total time: 8.3 seconds
Throughput: 1,204,819 messages/second
Average latency: 0.83us
Benchmark: Actor Creation
Creating and stopping actors
Results:
10,000 actors created in 89ms
Creation rate: 112,359 actors/second
Memory per actor: 896 bytes
Benchmark: Ask Pattern
Request-reply round trips
Results:
100,000 ask operations
Average round-trip: 4.2us
P99 round-trip: 12.1us
Real World Outcome
After completing this project, you will have:
- A working actor framework that you can use for concurrent applications
- Deep understanding of message-passing concurrency vs shared-state concurrency
- Experience with lock-free data structures in a real context
- Foundation for building distributed systems (actors naturally distribute)
- Portfolio piece demonstrating advanced concurrency skills
Solution Architecture
High-Level Design
+------------------------------------------------------------------------+
| ActorSystem |
+------------------------------------------------------------------------+
| |
| +---------------------------+ +---------------------------+ |
| | Actor Registry | | Thread Pool | |
| | (name -> ActorCell) | | (from Project 3) | |
| +---------------------------+ +---------------------------+ |
| | | |
| v v |
| +--------------------------------------------------------------------+ |
| | Actor Scheduling | |
| | When actor has messages: | |
| | 1. Mark as scheduled | |
| | 2. Submit to thread pool | |
| | 3. Worker runs actor.processMailbox() | |
| +--------------------------------------------------------------------+ |
| |
| +--------------------------------------------------------------------+ |
| | ActorCell (one per actor) | |
| | +----------------+ +----------------+ +------------------------+ | |
| | | Actor Instance | | Mailbox | | Supervision State | | |
| | | (user code) | | (lock-free Q) | | - parent ref | | |
| | | | | | | - child refs | | |
| | | - state | | +------------+ | | - restart policy | | |
| | | - receive() | | | Message 1 | | | | | |
| | | | | | Message 2 | | | | | |
| | | | | | Message 3 | | | | | |
| | +----------------+ | +------------+ | +------------------------+ | |
| | +----------------+ | |
| +--------------------------------------------------------------------+ |
| |
+------------------------------------------------------------------------+
Message Flow:
sender.tell(msg)
|
v
+------------------+
| Get target's |
| ActorCell |
+--------+---------+
|
v
+------------------+
| mailbox.enqueue |
| (lock-free push) |
+--------+---------+
|
| if (!scheduled.exchange(true))
v
+------------------+
| threadPool.submit|
| (processMailbox) |
+--------+---------+
|
[... worker thread picks up ...]
|
v
+------------------+
| while (msg = mailbox.pop()) |
| actor.dispatch(msg) |
+------------------+
|
v
scheduled.store(false)
if (mailbox.hasMore()) reschedule
Key Components
1. Actor Base Class
class Actor {
public:
virtual ~Actor() = default;
// Lifecycle hooks (override in subclass)
virtual void preStart() {}
virtual void postStop() {}
virtual void preRestart(std::exception_ptr reason) { postStop(); }
virtual void postRestart(std::exception_ptr reason) { preStart(); }
// Supervision hooks
virtual void onChildFailed(ActorRef child, std::exception_ptr error) {
// Default: apply supervisor strategy
}
protected:
// Available to subclasses
ActorRef self() const { return self_; }
ActorRef parent() const { return parent_; }
ActorSystem& system() const { return *system_; }
// Spawning children
template<typename A, typename... Args>
ActorRef spawn(std::string name, Args&&... args);
// Supervision
void supervise(ActorRef child, SupervisionStrategy strategy);
// Replying to ask() requests
template<typename T>
void reply(T&& response);
// Stop self or children
void stop();
void stop(ActorRef child);
private:
friend class ActorCell;
friend class ActorSystem;
ActorRef self_;
ActorRef parent_;
ActorSystem* system_ = nullptr;
ActorRef currentSender_; // Set during message processing
};
2. ActorRef (Handle)
class ActorRef {
public:
ActorRef() = default; // Null ref
// Tell (fire and forget)
template<typename M>
void tell(M&& message) const {
tell(std::forward<M>(message), ActorRef{});
}
template<typename M>
void tell(M&& message, ActorRef sender) const {
if (cell_) {
cell_->enqueue(Envelope{
std::make_any<std::decay_t<M>>(std::forward<M>(message)),
sender
});
}
}
// Ask (request-reply)
template<typename R, typename M>
std::future<R> ask(M&& message) const {
auto promise = std::make_shared<std::promise<R>>();
auto future = promise->get_future();
// Create temporary actor to receive reply
auto tempActor = system_->spawnAnonymous<PromiseActor<R>>(promise);
tell(std::forward<M>(message), tempActor);
return future;
}
// Identity
std::string path() const;
bool operator==(const ActorRef& other) const;
explicit operator bool() const { return cell_ != nullptr; }
private:
friend class ActorCell;
friend class ActorSystem;
std::shared_ptr<ActorCell> cell_;
ActorSystem* system_ = nullptr;
};
3. Mailbox (Lock-Free Queue)
class Mailbox {
public:
// Uses lock-free MPSC queue (multiple producers, single consumer)
// Single consumer because only one thread processes this actor at a time
void enqueue(Envelope message) {
queue_.push(std::move(message));
}
std::optional<Envelope> tryDequeue() {
return queue_.pop();
}
bool empty() const {
return queue_.empty();
}
private:
// From Project 6, but MPSC variant
LockFreeMPSCQueue<Envelope> queue_;
};
4. ActorCell (Actor Container)
class ActorCell : public std::enable_shared_from_this<ActorCell> {
public:
ActorCell(std::unique_ptr<Actor> actor,
ActorRef parent,
std::string name,
ActorSystem* system);
void enqueue(Envelope message) {
mailbox_.enqueue(std::move(message));
scheduleIfNeeded();
}
void processMailbox() {
// Process up to N messages per scheduling
constexpr int BATCH_SIZE = 100;
for (int i = 0; i < BATCH_SIZE; ++i) {
auto msg = mailbox_.tryDequeue();
if (!msg) break;
try {
actor_->currentSender_ = msg->sender;
dispatch(msg->message);
} catch (...) {
handleFailure(std::current_exception());
}
}
scheduled_.store(false, std::memory_order_release);
// Check if more messages arrived while processing
if (!mailbox_.empty()) {
scheduleIfNeeded();
}
}
private:
void scheduleIfNeeded() {
bool expected = false;
if (scheduled_.compare_exchange_strong(expected, true,
std::memory_order_acq_rel)) {
system_->schedule(shared_from_this());
}
}
void dispatch(const std::any& message);
void handleFailure(std::exception_ptr error);
std::unique_ptr<Actor> actor_;
Mailbox mailbox_;
std::atomic<bool> scheduled_{false};
std::string name_;
std::string path_;
ActorRef parent_;
std::vector<ActorRef> children_;
SupervisionStrategy strategy_ = SupervisionStrategy::Restart;
ActorSystem* system_;
};
5. ActorSystem
class ActorSystem {
public:
explicit ActorSystem(size_t numThreads = std::thread::hardware_concurrency())
: threadPool_(numThreads) {}
~ActorSystem() {
shutdown();
}
// Spawn top-level actor
template<typename A, typename... Args>
ActorRef spawn(std::string name, Args&&... args) {
return spawnChild<A>(userGuardian_, std::move(name),
std::forward<Args>(args)...);
}
// Look up actor by path
ActorRef lookup(const std::string& path);
// Graceful shutdown
void shutdown();
// Wait for completion
void awaitTermination();
private:
friend class ActorCell;
friend class Actor;
template<typename A, typename... Args>
ActorRef spawnChild(ActorRef parent, std::string name, Args&&... args) {
auto actor = std::make_unique<A>(std::forward<Args>(args)...);
auto cell = std::make_shared<ActorCell>(
std::move(actor), parent, std::move(name), this
);
registerActor(cell);
cell->actor_->preStart();
return ActorRef{cell, this};
}
void schedule(std::shared_ptr<ActorCell> cell) {
threadPool_.submit([cell] {
cell->processMailbox();
});
}
void registerActor(std::shared_ptr<ActorCell> cell);
void unregisterActor(const std::string& path);
WorkStealingThreadPool threadPool_; // From Project 3
std::unordered_map<std::string, std::shared_ptr<ActorCell>> registry_;
std::mutex registryMutex_;
ActorRef userGuardian_; // Root of user actor hierarchy
};
Data Structures
| Structure | Implementation | Purpose |
|---|---|---|
| Envelope | struct { std::any message; ActorRef sender; } |
Wrap message with sender info |
| Mailbox | Lock-free MPSC queue | Per-actor message buffer |
| ActorCell | Actor + Mailbox + metadata | Complete actor state |
| Registry | unordered_map<string, ActorCell*> |
Path-based lookup |
| Supervision | Parent-child links + policy enum | Failure handling |
Algorithm Overview
Message Sending (Tell)
tell(message, sender):
1. Look up target actor's cell (O(1) from ActorRef)
2. Wrap message in Envelope with sender
3. Enqueue to target's mailbox (lock-free push)
4. If actor not scheduled:
a. CAS scheduled flag from false to true
b. If successful, submit to thread pool
Message Processing
processMailbox():
for i = 0 to BATCH_SIZE:
msg = mailbox.tryPop()
if msg is null: break
try:
set currentSender to msg.sender
dispatch msg to appropriate receive() method
catch exception:
notify parent supervisor
apply supervision strategy
scheduled = false (release)
if mailbox not empty:
reschedule self
Supervision on Failure
handleFailure(exception):
1. Stop current message processing
2. Notify parent: parent.onChildFailed(self, exception)
3. Parent applies strategy:
- Restart: preRestart(), create new instance, postRestart()
- Stop: postStop(), remove from hierarchy
- Escalate: parent throws, its parent handles
4. If Restart: drain mailbox or process pending messages
Implementation Guide
Development Environment Setup
Required Compiler and Tools
# Ubuntu/Debian
sudo apt install g++-11 cmake ninja-build
# macOS
brew install gcc@11 cmake ninja
# Verify C++17 support
g++ --version # Should be 11+ for best C++17/20 support
# Install sanitizers (usually included with modern compilers)
# ThreadSanitizer is critical for testing
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(ActorFramework CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Optimization for release
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
# Debug with sanitizers
set(CMAKE_CXX_FLAGS_DEBUG "-g -O1 -fsanitize=thread")
# Library
add_library(actors
src/actor_system.cpp
src/actor_cell.cpp
src/actor_ref.cpp
src/mailbox.cpp
)
target_include_directories(actors PUBLIC include)
# Benchmarks
add_executable(actor_benchmark
benchmark/throughput.cpp
benchmark/latency.cpp
)
target_link_libraries(actor_benchmark actors pthread)
# Tests
enable_testing()
add_executable(actor_tests
tests/actor_test.cpp
tests/supervision_test.cpp
tests/ask_test.cpp
)
target_link_libraries(actor_tests actors pthread)
add_test(NAME actor_tests COMMAND actor_tests)
Project Structure
actor_framework/
include/
actor.hpp # Actor base class
actor_ref.hpp # ActorRef handle
actor_system.hpp # ActorSystem container
actor_cell.hpp # Internal: actor + mailbox
mailbox.hpp # Lock-free message queue
envelope.hpp # Message wrapper
supervision.hpp # Supervision strategies
src/
actor_system.cpp # ActorSystem implementation
actor_cell.cpp # ActorCell implementation
actor_ref.cpp # ActorRef implementation
mailbox.cpp # Mailbox implementation
tests/
actor_test.cpp # Basic actor tests
supervision_test.cpp # Failure handling tests
ask_test.cpp # Request-reply tests
stress_test.cpp # Concurrent load tests
benchmark/
throughput.cpp # Messages per second
latency.cpp # Round-trip timing
examples/
ping_pong.cpp # Classic actor example
counter.cpp # Simple state example
supervisor.cpp # Supervision hierarchy
CMakeLists.txt
README.md
Core Question
The Core Question You’re Answering:
How can we structure concurrent programs so that data races are impossible by construction, rather than prevented by careful use of locks?
The actor model answers this by:
- Encapsulating state within actors (no shared mutable state)
- Communication only via immutable messages (no pointer sharing)
- Sequential processing within each actor (no concurrent access to actor state)
Concepts You Must Understand First
Before implementing, verify you can answer these questions:
- Why does the actor model eliminate data races?
- Each actor processes one message at a time
- State is never shared between actors
- Messages are copied/moved, not referenced
- How does a lock-free queue enable efficient mailboxes?
- Multiple senders can enqueue concurrently (MPSC)
- No mutex contention on message passing
- Bounded memory usage with ring buffer
- What is the difference between tell and ask?
- Tell: sender continues immediately, no response
- Ask: sender gets a future, waits for response
- How do supervision trees handle failures?
- Parent actors supervise children
- Failure propagates up the tree
- Supervisor decides: restart, stop, or escalate
- Why use std::any or std::variant for messages?
- Mailbox must hold arbitrary message types
- Type erasure allows single queue type
- Runtime dispatch to appropriate handler
Questions to Guide Your Design
Actor Lifecycle:
- When is an actor’s state initialized (constructor vs preStart)?
- How do you handle messages sent during actor restart?
- When should an actor stop itself vs be stopped by parent?
Mailbox Design:
- MPSC (multiple producer, single consumer) vs MPMC?
- Bounded vs unbounded queue?
- What happens when mailbox is full?
Message Dispatch:
- How do you route messages to the correct handler?
- What if a message type has no handler?
- How do you handle exceptions in handlers?
Scheduling:
- How many messages should an actor process before yielding?
- How do you ensure fairness among actors?
- How do you handle actors with huge mailbox backlogs?
Supervision:
- Where do you store supervision configuration?
- How do you restart an actor without losing its ActorRef?
- How do you propagate errors up the supervision tree?
Thinking Exercise
Before coding, trace through this scenario on paper:
Scenario: Three actors - Counter, Worker, Supervisor
1. Supervisor spawns Counter and Worker as children
2. Worker sends Increment(5) to Counter
3. Worker sends Increment(3) to Counter
4. Worker crashes (throws exception)
5. Supervisor restarts Worker
6. New Worker sends GetCount to Counter
7. Counter replies with Count(8)
Draw:
- The actor hierarchy
- Message flow timeline
- State changes in each actor
- Thread assignments during processing
Questions to answer:
- Which thread runs each actor?
- What happens to Worker’s pending messages on crash?
- How does the new Worker get Counter’s ActorRef?
- What if Counter crashes while processing Increment?
Hints in Layers
Hint 1: Starting Point (Conceptual Direction)
Start with the simplest possible actor system:
- Single-threaded (no thread pool yet)
- One message type (use
std::function<void()>for simplicity) - No supervision
- Synchronous processing (process message immediately on tell)
This gives you a working mental model before adding complexity.
Hint 2: Add Asynchronous Processing
Introduce the mailbox and scheduling:
- Add a queue to buffer messages
- Process queue in a loop
- Add a flag to track if actor is scheduled
- Submit to thread pool when new message arrives and not scheduled
Key insight: The scheduling flag is the key to efficiency. Without it, you’d submit to thread pool on every message.
Hint 3: Message Type Erasure
Use std::any for flexibility:
struct Envelope {
std::any message;
ActorRef sender;
};
// In actor subclass, provide typed handlers
void receive(const Increment& m) { ... }
void receive(const GetCount& m) { ... }
// Dispatch via type checking
void dispatch(const std::any& msg) {
if (auto* m = std::any_cast<Increment>(&msg)) {
receive(*m);
} else if (auto* m = std::any_cast<GetCount>(&msg)) {
receive(*m);
} else {
// Unknown message type
}
}
For better performance, consider std::variant if message types are known at compile time.
Hint 4: Implementing Ask Pattern
The ask pattern requires a temporary actor to receive the reply:
template<typename R>
class PromiseActor : public Actor {
std::shared_ptr<std::promise<R>> promise_;
public:
PromiseActor(std::shared_ptr<std::promise<R>> p) : promise_(p) {}
void receive(const R& reply) {
promise_->set_value(reply);
stop(); // Self-destruct after receiving reply
}
};
template<typename R, typename M>
std::future<R> ActorRef::ask(M message) {
auto promise = std::make_shared<std::promise<R>>();
auto future = promise->get_future();
auto tempActor = system_->spawnAnonymous<PromiseActor<R>>(promise);
tell(std::move(message), tempActor);
return future;
}
Don’t forget timeout handling! A stuck actor will never respond.
Interview Questions
-
“Explain the actor model and how it prevents data races.”
Expected answer: The actor model isolates state within actors. Each actor has a mailbox for incoming messages and processes them one at a time. Since state is never shared between actors (messages are copied, not referenced), and processing is sequential within each actor, data races are structurally impossible. Multiple actors can run concurrently on different threads, but they interact only through message passing.
-
“How would you implement the ‘ask’ pattern in an actor system?”
Expected answer: The ask pattern returns a future for request-reply communication. Implementation:
- Create a promise/future pair
- Spawn a temporary “reply-to” actor that holds the promise
- Send the message with reply-to as the sender
- When target processes message, it sends reply to sender (the temp actor)
- Temp actor fulfills the promise and stops itself
- Original caller gets result via future.get()
Also mention: timeout handling, memory management of temp actor.
-
“What are the tradeoffs between actors and traditional mutex-based concurrency?”
Expected answer:
- Actors: No data races by design, but message passing overhead, potential message ordering issues, learning curve
- Mutexes: Direct shared access, familiar model, but deadlock risk, composition difficulty
- Actors excel at: distributed systems, fault tolerance, many independent entities
- Mutexes excel at: tight coupling, high-frequency shared access, simple critical sections
-
“How would you design supervision in an actor system?”
Expected answer: Actors form a hierarchy where each actor supervises its children. When a child fails:
- Child catches exception, notifies parent
- Parent applies supervision strategy:
- Restart: Call preRestart, create new instance, call postRestart
- Stop: Call postStop, remove from hierarchy
- Escalate: Parent re-throws, its parent handles
- Optionally resume mailbox processing or drop pending messages
Key insight: Separation of concerns - business logic in actor, error handling in supervisor.
-
“How would you scale an actor system across multiple machines?”
Expected answer: Location transparency is key. ActorRefs should work the same whether the actor is local or remote:
- Serialize messages (Protocol Buffers, FlatBuffers)
- ActorRef includes location info (host:port)
- Remote send goes through network layer
- Remote node deserializes and delivers to local actor
- Cluster membership and actor discovery (registry service)
- Handle network partitions (timeouts, retries, circuit breakers)
Books That Will Help
| Topic | Book | Relevant Chapters |
|---|---|---|
| Actor Model Theory | “Reactive Design Patterns” by Roland Kuhn | Chapters 1-4 (Actor fundamentals) |
| Erlang/OTP Patterns | “Designing for Scalability with Erlang/OTP” by Cesarini & Vinoski | Chapters 3-6 (Supervision) |
| C++ Concurrency | “C++ Concurrency in Action” by Williams | Chapter 4 (Message passing) |
| Lock-Free Queues | “C++ Concurrency in Action” by Williams | Chapter 7 (Lock-free data structures) |
| Akka Implementation | “Akka in Action” by Roestenburg | Chapters 2-5 (Actor patterns) |
| Distributed Systems | “Designing Data-Intensive Applications” by Kleppmann | Chapter 8 (Distributed actors) |
Implementation Phases
Phase 1: Basic Actors (Week 1)
Goal: Single-threaded actor system with tell().
// Milestone: This should work
class EchoActor : public Actor {
public:
void receive(const std::string& msg) {
std::cout << "Echo: " << msg << std::endl;
}
};
ActorSystem system(1);
auto echo = system.spawn<EchoActor>("echo");
echo.tell(std::string("Hello"));
echo.tell(std::string("World"));
// Output: Echo: Hello\nEcho: World
Checklist:
- Actor base class with lifecycle hooks
- ActorRef with tell()
- Simple mailbox (std::queue + mutex is fine initially)
- ActorSystem spawns actors
- Single worker thread processes actors
Phase 2: Lock-Free Mailbox (Week 2)
Goal: Replace mutex-protected queue with lock-free MPSC.
// Reuse or adapt your Project 6 lock-free queue
// Key difference: MPSC (multiple producer, single consumer)
// because each actor has one consumer (the actor itself)
Checklist:
- Implement or adapt MPSC lock-free queue
- Replace mutex-protected mailbox
- Verify no data races with ThreadSanitizer
- Benchmark: should see throughput improvement
Phase 3: Thread Pool Integration (Week 3)
Goal: Actors run on your work-stealing thread pool.
// Scheduling logic
void ActorCell::enqueue(Envelope msg) {
mailbox_.push(std::move(msg));
bool expected = false;
if (scheduled_.compare_exchange_strong(expected, true)) {
system_->schedule(shared_from_this());
}
}
void ActorCell::processMailbox() {
// Process batch of messages
for (int i = 0; i < BATCH_SIZE; ++i) {
auto msg = mailbox_.pop();
if (!msg) break;
dispatch(*msg);
}
scheduled_.store(false);
if (!mailbox_.empty()) {
enqueue({}); // Reschedule if more work
}
}
Checklist:
- Integrate with Project 3 thread pool
- Implement scheduling flag (atomic CAS)
- Batch processing (N messages per schedule)
- Benchmark scaling with thread count
Phase 4: Ask Pattern (Week 4)
Goal: Request-reply with futures.
// Milestone: This should work
class Calculator : public Actor {
public:
void receive(const Add& msg) {
reply(Result{msg.a + msg.b});
}
};
auto calc = system.spawn<Calculator>("calc");
auto future = calc.ask<Result>(Add{2, 3});
Result r = future.get();
assert(r.value == 5);
Checklist:
- PromiseActor for receiving replies
- ask() method on ActorRef
- reply() method on Actor
- Timeout handling for stuck asks
Phase 5: Supervision (Week 5)
Goal: Parent-child relationships with restart policies.
// Milestone: This should work
class Worker : public Actor {
public:
void receive(const DoWork& msg) {
if (msg.shouldFail) {
throw std::runtime_error("Simulated failure");
}
// Process work
}
};
class Supervisor : public Actor {
public:
void preStart() override {
auto worker = spawn<Worker>("worker");
supervise(worker, SupervisionStrategy::Restart);
}
void onChildFailed(ActorRef child, std::exception_ptr e) override {
std::cout << "Child failed, will restart" << std::endl;
}
};
// Worker fails and is automatically restarted
Checklist:
- Parent-child tracking in ActorCell
- SupervisionStrategy enum (Restart, Stop, Escalate)
- Exception handling in message dispatch
- preRestart/postRestart hooks
- Integration tests for failure scenarios
Phase 6: Polish and Optimization (Week 6)
Goals:
- Handle edge cases
- Add actor naming and lookup
- Performance tuning
- Documentation
Checklist:
- Actor path naming (/user/supervisor/worker)
- Lookup by path
- Graceful shutdown (stop all actors)
- Memory leak verification (valgrind)
- Documentation and examples
- Comprehensive benchmark suite
Key Implementation Decisions
Decision 1: Message Type Representation
Options:
std::any- Runtime flexibility, slower dispatchstd::variant- Compile-time safety, faster dispatch- Virtual base class - OOP pattern, heap allocation
Recommendation: Start with std::any for flexibility. Optimize to std::variant for hot paths once working.
Decision 2: MPSC vs MPMC Mailbox
Analysis: Each actor processes its own mailbox, so there’s only one consumer. MPSC (multiple producer, single consumer) is simpler and faster than full MPMC.
Recommendation: Use MPSC lock-free queue.
Decision 3: Scheduling Granularity
Options:
- Process all messages per schedule (unfair)
- Process one message per schedule (too much overhead)
- Process N messages per schedule (balance)
Recommendation: Process batch of ~100 messages, then yield. Prevents any single actor from starving others.
Decision 4: Actor Identity
Options:
- Unique integer ID
- Hierarchical path string (/user/app/worker-1)
- UUID
Recommendation: Hierarchical paths (like Akka). Readable, debuggable, natural for supervision hierarchy.
Testing Strategy
Unit Tests
TEST(ActorTest, BasicTell) {
ActorSystem system(1);
std::atomic<int> received{0};
class Counter : public Actor {
std::atomic<int>& count_;
public:
Counter(std::atomic<int>& c) : count_(c) {}
void receive(const int& n) { count_ += n; }
};
auto counter = system.spawn<Counter>("counter", std::ref(received));
counter.tell(5);
counter.tell(3);
// Wait for processing
std::this_thread::sleep_for(100ms);
EXPECT_EQ(received.load(), 8);
}
TEST(ActorTest, AskPattern) {
ActorSystem system(4);
class Echo : public Actor {
public:
void receive(const std::string& msg) {
reply(msg + "!");
}
};
auto echo = system.spawn<Echo>("echo");
auto future = echo.ask<std::string>(std::string("Hello"));
EXPECT_EQ(future.get(), "Hello!");
}
Supervision Tests
TEST(SupervisionTest, RestartOnFailure) {
ActorSystem system(4);
std::atomic<int> restartCount{0};
class FragileWorker : public Actor {
std::atomic<int>& restarts_;
int failCountdown_ = 3;
public:
FragileWorker(std::atomic<int>& r) : restarts_(r) {}
void postRestart(std::exception_ptr) override {
restarts_++;
}
void receive(const Ping&) {
if (--failCountdown_ > 0) {
throw std::runtime_error("Intentional failure");
}
reply(Pong{});
}
};
class Supervisor : public Actor {
std::atomic<int>& restarts_;
public:
Supervisor(std::atomic<int>& r) : restarts_(r) {}
void preStart() override {
auto worker = spawn<FragileWorker>("worker", std::ref(restarts_));
supervise(worker, SupervisionStrategy::Restart);
}
};
auto sup = system.spawn<Supervisor>("supervisor", std::ref(restartCount));
auto worker = system.lookup("/user/supervisor/worker");
// Trigger failures and restarts
worker.tell(Ping{});
worker.tell(Ping{});
worker.tell(Ping{});
std::this_thread::sleep_for(500ms);
EXPECT_GE(restartCount.load(), 2);
}
Stress Tests
TEST(StressTest, HighThroughput) {
ActorSystem system(8);
std::atomic<int64_t> processed{0};
constexpr int NUM_MESSAGES = 1'000'000;
class Counter : public Actor {
std::atomic<int64_t>& count_;
public:
Counter(std::atomic<int64_t>& c) : count_(c) {}
void receive(const int&) { count_++; }
};
auto counter = system.spawn<Counter>("counter", std::ref(processed));
auto start = std::chrono::high_resolution_clock::now();
// Multiple sender threads
std::vector<std::thread> senders;
for (int t = 0; t < 8; ++t) {
senders.emplace_back([&counter, NUM_MESSAGES]() {
for (int i = 0; i < NUM_MESSAGES / 8; ++i) {
counter.tell(1);
}
});
}
for (auto& t : senders) t.join();
// Wait for all to be processed
while (processed.load() < NUM_MESSAGES) {
std::this_thread::sleep_for(10ms);
}
auto end = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "Throughput: " << (NUM_MESSAGES * 1000 / ms) << " msg/sec\n";
EXPECT_GT(NUM_MESSAGES * 1000 / ms, 500'000); // At least 500K/sec
}
Sanitizer Testing
# Thread Sanitizer (critical for actor systems)
cmake -DCMAKE_BUILD_TYPE=Debug ..
make actor_tests
./actor_tests
# Should report zero data races
# Address Sanitizer (memory issues)
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address" ..
make actor_tests
./actor_tests
Common Pitfalls & Debugging
Pitfall 1: Forgetting to Schedule Actor
Problem: Messages are enqueued but never processed.
// WRONG: Enqueue without scheduling
void ActorCell::enqueue(Envelope msg) {
mailbox_.push(std::move(msg));
// Missing: schedule the actor!
}
// RIGHT: Schedule if not already scheduled
void ActorCell::enqueue(Envelope msg) {
mailbox_.push(std::move(msg));
scheduleIfNeeded(); // Critical!
}
Symptom: Actor appears to ignore messages.
Fix: Always check scheduling flag after enqueue.
Pitfall 2: Race in Scheduling Flag
Problem: Double-scheduling or missed scheduling.
// WRONG: Non-atomic check-then-act
void scheduleIfNeeded() {
if (!scheduled_) {
scheduled_ = true;
system_->schedule(this);
}
}
// RIGHT: Atomic compare-and-exchange
void scheduleIfNeeded() {
bool expected = false;
if (scheduled_.compare_exchange_strong(expected, true,
std::memory_order_acq_rel)) {
system_->schedule(shared_from_this());
}
}
Symptom: Spurious duplicate processing or dropped messages.
Fix: Use CAS for the scheduling flag.
Pitfall 3: Deadlock in Ask Pattern
Problem: Calling ask() and blocking on result inside an actor.
// WRONG: Blocks the only thread that could process the reply!
class BadActor : public Actor {
void receive(const Request& r) {
auto result = other_.ask<Result>(Query{}).get(); // DEADLOCK
reply(result);
}
};
Symptom: System hangs, ask never completes.
Fix: Either:
- Use tell() and handle reply in separate message
- Use async continuation instead of blocking get()
- Never block inside an actor
Pitfall 4: Shared State Between Actors
Problem: Passing pointers in messages, breaking actor isolation.
// WRONG: Shared pointer allows concurrent access!
struct BadMessage {
std::shared_ptr<std::vector<int>> data; // SHARED MUTABLE STATE!
};
// RIGHT: Move or copy data, no sharing
struct GoodMessage {
std::vector<int> data; // Moved into message, actor owns it
};
Symptom: Data races, inconsistent state.
Fix: Messages should own their data. No shared pointers to mutable data.
Pitfall 5: Actor Self-Reference Cycle
Problem: Actor holds strong reference to itself, preventing cleanup.
// WRONG: Prevents actor destruction
class LeakyActor : public Actor {
ActorRef selfRef_; // Strong reference to self!
void preStart() override {
selfRef_ = self(); // Creates cycle
}
};
Symptom: Actors never destroyed, memory leak.
Fix: Don’t store self references. Use self() method when needed.
Pitfall 6: Lost Exception in Fire-and-Forget
Problem: Exception in actor is silently swallowed with tell().
// Exception here goes nowhere with tell()
class FragileActor : public Actor {
void receive(const Work& w) {
throw std::runtime_error("Oops");
// Who sees this error?
}
};
// Without supervision, error is lost
actor.tell(Work{});
Symptom: Silent failures, inconsistent state.
Fix: Implement proper supervision. Log unhandled exceptions. Consider making supervision mandatory.
Debugging Tips
- Add logging: Log actor lifecycle events (spawn, message receive, stop)
- Use actor paths: Hierarchical paths make logs readable
- ThreadSanitizer: Run early and often, catches races immediately
- Message tracing: Log message flow with timestamps and actor paths
- Deadlock detection: Add timeout to ask(), log when exceeded
Extensions & Challenges
Extension 1: Typed Actors
Add compile-time type safety to message handling:
template<typename... Messages>
class TypedActor : public Actor {
// Only accepts Messages... types
// Compile error if you send wrong type
};
class Counter : public TypedActor<Increment, GetCount, Reset> {
// Must implement receive() for each type
};
Extension 2: Actor Pools
Implement router actors that distribute work:
// Round-robin distribution
auto pool = system.spawnPool<Worker>(8); // 8 workers
pool.tell(Work{}); // Goes to next worker in rotation
// Broadcast
pool.broadcast(Shutdown{}); // Goes to all workers
Extension 3: Persistence (Event Sourcing)
Add state persistence for fault tolerance:
class PersistentActor : public Actor {
void persist(const Event& event) {
// Save event to journal
journal_.append(event);
// Apply event to state
apply(event);
}
void recover() {
// Replay all events from journal
for (const auto& event : journal_.read()) {
apply(event);
}
}
};
Extension 4: Remote Actors
Add network transparency:
// Local actor
auto local = system.spawn<Worker>("worker");
// Remote actor (on another machine)
auto remote = system.remote("192.168.1.100:8080")
.spawn<Worker>("worker");
// Same API!
local.tell(Work{});
remote.tell(Work{}); // Serializes and sends over network
Extension 5: Backpressure
Handle mailbox overflow gracefully:
enum class OverflowStrategy {
DropOldest, // Discard oldest message
DropNewest, // Discard new message
Block, // Block sender (needs async support)
Fail // Throw exception
};
auto actor = system.spawn<Worker>("worker",
MailboxConfig{
.capacity = 1000,
.overflow = OverflowStrategy::DropOldest
});
Challenge: Implement Virtual Actors
Microsoft Orleans-style virtual actors that are automatically activated/deactivated:
// Virtual actor - always exists conceptually
// Framework activates instance when needed
auto player = grains.get<PlayerGrain>("player-123");
player.tell(Attack{}); // Activates if not in memory
// After idle timeout, actor is deactivated
// State is persisted, reloaded on next access
Real-World Connections
How Akka Does It
Akka (Scala/Java) is the most widely-used actor framework:
| Akka Concept | Your Implementation |
|---|---|
ActorSystem |
ActorSystem |
ActorRef |
ActorRef |
Props |
Template parameters to spawn() |
Dispatcher |
Thread pool integration |
SupervisorStrategy |
SupervisionStrategy enum |
Mailbox |
Lock-free queue |
tell / ! |
tell() |
ask / ? |
ask() |
How Erlang Does It
Erlang’s actor model (processes) is built into the language:
| Erlang Feature | Your Implementation |
|---|---|
spawn(fun) |
system.spawn<Actor>() |
Pid |
ActorRef |
! (send) |
tell() |
receive block |
receive() method |
| Supervisor module | Supervision hierarchy |
gen_server |
Actor base class |
| Process mailbox | Lock-free queue |
Production Systems Using Actors
| System | Technology | Scale |
|---|---|---|
| Erlang | 2B users, 100B messages/day | |
| Discord | Elixir/Erlang | 19M concurrent users |
| Akka | 500M members | |
| PayPal | Akka | Financial transactions |
| Roblox | Custom C++ actors | Game servers |
Resources
Primary References
| Resource | Description |
|---|---|
| “C++ Concurrency in Action” | Chapter 4 covers message passing patterns |
| “Reactive Design Patterns” | Comprehensive actor pattern guide |
| Akka Documentation | Production actor framework reference |
| CAF: C++ Actor Framework | Open source C++ actor library |
Code to Study
| Project | What to Learn |
|---|---|
| CAF | Production C++ actor implementation |
| SObjectizer | Alternative C++ actor approach |
| rotor | Lightweight C++ actors |
| Akka Source | Reference implementation (Scala) |
Supplemental Reading
| Topic | Resource |
|---|---|
| Actor Model Theory | Carl Hewitt’s original paper |
| Erlang OTP | “Learn You Some Erlang” (free online) |
| Lock-free Queues | Project 6 of this guide |
| Thread Pools | Project 3 of this guide |
Self-Assessment Checklist
Understanding
- I can explain why the actor model prevents data races
- I can describe the three axioms of actors
- I understand the difference between tell and ask patterns
- I can explain how supervision trees provide fault tolerance
- I understand why mailboxes should be lock-free
Implementation
- Actor base class with lifecycle hooks works correctly
- ActorRef supports tell() with type-erased messages
- Lock-free mailbox handles concurrent producers
- Thread pool executes actors with proper scheduling
- Ask pattern returns futures that resolve with replies
- Supervision restarts failed child actors
Testing
- Unit tests cover basic actor operations
- Supervision tests verify failure handling
- Stress tests demonstrate high throughput
- All tests pass under Thread Sanitizer
- No memory leaks detected by Address Sanitizer
Performance
- Throughput exceeds 500K messages/second
- Ask latency under 50 microseconds average
- Scales linearly up to 8+ threads
- Memory per actor under 2KB
Extensions (Optional)
- Typed actors with compile-time message checking
- Actor pools with routing strategies
- Backpressure handling for mailbox overflow
- Actor naming and path-based lookup
Submission / Completion Criteria
Minimum Requirements
Your implementation is complete when:
- Core Actor Functionality:
- Actors can be spawned with custom behavior
- Messages are delivered via tell()
- Actors process messages sequentially
- ActorRefs can be passed in messages
- Message Handling:
- Type-erased message storage (std::any or std::variant)
- Dispatch to appropriate receive() methods
- Unknown message types handled gracefully
- Concurrency:
- Lock-free mailbox (MPSC queue)
- Thread pool execution of actors
- Proper scheduling (no double-schedule, no missed schedule)
- No data races (TSan clean)
- Ask Pattern:
- ask() returns std::future
- reply() sends response to sender
- Timeout handling for stuck asks
- Supervision:
- Parent-child actor hierarchy
- Configurable restart policy
- onChildFailed() notification
- preRestart/postRestart hooks
Deliverables
actor_framework/
include/
actor.hpp # Actor base class
actor_ref.hpp # ActorRef handle
actor_system.hpp # ActorSystem container
actor_cell.hpp # Internal: actor + mailbox
mailbox.hpp # Lock-free message queue
envelope.hpp # Message wrapper
supervision.hpp # Supervision strategies
src/
actor_system.cpp
actor_cell.cpp
actor_ref.cpp
mailbox.cpp
tests/
actor_test.cpp
supervision_test.cpp
ask_test.cpp
stress_test.cpp
benchmark/
throughput.cpp
latency.cpp
examples/
ping_pong.cpp
counter.cpp
supervisor.cpp
CMakeLists.txt
README.md
Grading Rubric (Self-Assessment)
| Component | Points | Criteria |
|---|---|---|
| Actor lifecycle | 15 | spawn, preStart, postStop work correctly |
| Message passing | 20 | tell() delivers, dispatch routes correctly |
| Lock-free mailbox | 15 | MPSC queue, no races |
| Thread pool integration | 15 | Proper scheduling, scales with cores |
| Ask pattern | 15 | futures work, reply() works |
| Supervision | 10 | Restart policy applied on failure |
| Testing | 5 | Comprehensive tests, TSan clean |
| Documentation | 5 | README, code comments |
Total: 100 points
Learning Milestones
Milestone 1: Basic Tell Works
Checkpoint: Single actor receives and processes messages.
What You’ve Proven:
- Actor base class is correct
- Message dispatch works
- Basic mailbox functions
Milestone 2: Multi-Actor Communication
Checkpoint: Multiple actors exchange messages.
What You’ve Proven:
- ActorRef can be passed in messages
- Actors can find and message each other
- No message loss under load
Milestone 3: Thread Pool Integration
Checkpoint: Actors run on thread pool with proper scheduling.
What You’ve Proven:
- Scheduling flag prevents double-scheduling
- Lock-free mailbox handles concurrent sends
- Actors scale across cores
Milestone 4: Ask Pattern Works
Checkpoint: Request-reply with futures completes correctly.
What You’ve Proven:
- Temporary reply actor works
- Promise/future integration correct
- Timeout handling in place
Milestone 5: Supervision Restarts Failed Actors
Checkpoint: Parent actor restarts crashed child.
What You’ve Proven:
- Exception handling in message dispatch
- Parent notification works
- Restart preserves actor identity (same ActorRef)
Estimated completion time: 4-6 weeks of focused effort
This project synthesizes your thread pool and lock-free queue knowledge into a high-level abstraction. The actor model is a powerful mental framework that will influence how you think about concurrent systems, even when not using actors directly.