Core Concepts

What Are Actors?

Actors are independent workers that communicate through messages. Instead of sharing memory and coordinating access with locks, each actor owns its data and interacts only by sending and receiving messages.

This simple model makes concurrent programming dramatically easier.

The Workers with Inboxes Mental Model

Think of actors like workers in an office:

  • Each has their own desk (private state nobody else can touch)
  • Each has an inbox where messages arrive
  • Each processes messages one at a time (no interruptions mid-task)
  • Each can send memos to other workers' inboxes

No worker ever reaches across to rummage through another worker's desk. All coordination happens through messages.


The Three Things Actors Do

Every actor does exactly three things:

1. Receive Messages

When a message arrives, the actor wakes up to handle it.

2. Update State

While handling a message, an actor can modify its private state. Because messages are processed one at a time, there's no risk of data races.

3. Send Messages

Actors communicate with other actors by sending messages to their handles.


Why This Matters: No Shared State Bugs

Consider traditional concurrent programming:

The Shared State Problem

// Traditional threading - careful coordination needed
let counter = Arc::new(Mutex::new(0));
let counter1 = counter.clone();

thread::spawn(move || {
    let mut val = counter1.lock().unwrap();
    *val += 1;  // Need the lock!
});

With mutexes, you need to think about lock ordering, deadlocks, and whether you've protected all the shared state.

With actors, this complexity vanishes:

// Actor model - messages processed one at a time
counter.mutate_on::<Increment>(|actor, _envelope| {
    actor.model.count += 1;  // Always safe
    Reply::ready()
});

When two Increment messages arrive, they're processed sequentially. No locks, no races.


Actors Are Lightweight

Unlike operating system threads, actors are cheap. They're Rust structs with a message queue, running on Tokio's async runtime.

You can create thousands:

for i in 0..10_000 {
    let mut actor = runtime.new_actor_with_name::<Session>(format!("session-{}", i));
    actor.mutate_on::<Request>(handle_request);
    actor.start().await;
}

Actors Are Isolated

Each actor runs independently. If one encounters an error, others continue normally. The supervision system handles failures gracefully.

This means:

  • Failures are contained — one broken actor doesn't crash your system
  • State is protected — no actor can corrupt another's data
  • Testing is simpler — test actors in isolation

For Experienced Developers

If you've used Actix or other actor frameworks, Acton's approach will feel familiar with key differences:

  • No async_trait boilerplate — Handlers use mutate_on and act_on methods
  • Envelope-based messaging — Handlers receive envelopes, not raw messages
  • Tokio-native — Built directly on Tokio, not a separate runtime

The mutate_on vs act_on distinction is key — it determines sequential vs concurrent handler execution.


Next

Messages and Handlers — The crucial difference between mutate_on and act_on

Previous
Next steps