Skip to content
📖 Auto vs RustBook contents

Actor Concurrency ​

Handling concurrent programming safely and efficiently is one of Auto's major goals. Concurrent programming, in which different parts of a program execute independently, and parallel programming, in which different parts of a program execute at the same time, are becoming increasingly important as more computers take advantage of their multiple processors.

Rust's approach to concurrency is built around threads, channels, and shared state with Mutex<T> and Arc<T>. Auto takes a different approach: the Actor model. In the actor model, concurrency is expressed through independent units of computation called actors that communicate exclusively by sending messages to each other. No actor can directly access another actor's state.

This model eliminates entire classes of concurrency bugs:

  • No data races — actors don't share memory
  • No deadlocks — there are no locks to deadlock on
  • No stale references — communication is via message passing, not shared pointers

Auto's ownership system still plays a crucial role: it ensures that messages sent between actors transfer ownership cleanly, preventing the sender from accidentally using data that has been sent to another actor.

Note: The Actor model is Auto's primary concurrency abstraction. For compatibility with Rust libraries, Auto also supports thread-based concurrency through use.rust. This chapter focuses on the Actor model, which is the idiomatic Auto approach.

Here are the topics we'll cover in this chapter:

  • How the Actor model differs from thread-based concurrency
  • How to create actors and send messages between them
  • Message-passing patterns: request/response, fan-out, and pipelines
  • Shared-state patterns when interop with Rust threads is needed
  • Concurrency specs: Send and marker specs

From Threads to Actors ​

In Rust, concurrency is based on threads — OS-level units of execution that share memory. Threads communicate through channels (message passing) or shared state protected by mutexes.

The Problem with Shared Memory ​

Thread-based concurrency with shared memory is powerful but error-prone:

  • Race conditions: threads accessing data in an inconsistent order
  • Deadlocks: two threads waiting for each other, preventing both from continuing
  • Subtle bugs that only happen in certain situations and are hard to reproduce

Rust's ownership system helps prevent many of these issues at compile time, but the mental model is still complex. You must reason about locks, lifetimes, and thread boundaries simultaneously.

The Actor Model ​

The Actor model, first described by Carl Hewitt in 1973, takes a different approach. An actor is the fundamental unit of computation with these properties:

  1. Each actor has its own private state — no other actor can access it
  2. Actors communicate exclusively by sending asynchronous messages
  3. When an actor receives a message, it can:
    • Send messages to other actors
    • Create new actors
    • Change its own state for the next message

This model maps naturally to many real-world concurrency problems and avoids the pitfalls of shared-memory concurrency.

Actor Model vs Thread Model ​

ConceptRust (Threads)Auto (Actors)
Unit of concurrencyThreadActor
CommunicationChannels or shared stateMessage passing
State sharingMutex + ArcNo sharing
Data racesPrevented by ownershipImpossible by design
DeadlocksPossibleImpossible (no locks)
Error handlingResult typesMessage-level errors

Creating Actors ​

In Auto, an actor is defined using the actor keyword. An actor encapsulates its own state and defines which messages it can receive.

Defining an Actor ​

A simple counter actor

The Counter actor maintains a private count field. The on Increment block handles incoming Increment messages. The on GetCount -> int block handles GetCount messages and returns the current count.

Key differences from Rust's approach:

  • No Arc<Mutex<T>> — the actor owns its state privately
  • No move closures — the actor model handles ownership transfer automatically
  • No .lock().unwrap() — no locks needed, messages are processed one at a time

Actor Lifecycle ​

Each actor goes through these phases:

  1. Creation — Counter.init() creates a new actor instance
  2. Running — the actor processes messages from its mailbox
  3. Shutdown — when all references are dropped, the actor stops

Sending Messages ​

There are two ways to send messages to an actor:

  • send — fire-and-forget (asynchronous, no response expected)
  • ask — request-response (waits for a return value)
auto
counter.send(Increment)         // fire and forget
let n = counter.ask(GetCount)   // waits for response

Message Passing Patterns ​

Basic Message Passing ​

Let's create a more complete example: a logger actor that receives messages and prints them.

A logger actor that prints messages

In Rust, you'd need to create a channel, define an enum for messages, spawn a thread, and manually match on incoming messages. Auto's actor model packages this into a more concise, purpose-built abstraction.

Fan-Out: Multiple Actors, One Message ​

A common pattern is broadcasting a message to multiple actors:

Fan-out: sending messages to multiple workers

Pipeline: Actors in Sequence ​

Messages can flow through a pipeline of actors, each performing a transformation:

A pipeline of actors processing data

State Management in Actors ​

Each actor maintains its own private state. When processing a message, the actor can modify its state, and those changes persist for the next message.

Accumulator Pattern ​

An actor that accumulates values

The Auto version is significantly simpler: no Arc, no Mutex, no thread management. The actor processes one message at a time, so there's no possibility of data races on total.

Actor with Complex State ​

A key-value store actor

Interop with Rust Threads ​

When you need to use Rust's thread-based concurrency (for example, to call a Rust library that uses threads), Auto provides access through use.rust:

Using Rust threads for interop

When to Use Actors vs Threads ​

ScenarioUse ActorsUse Threads
Independent servicesYesNo
Shared mutable stateAvoid with actorsMutex + Arc
CPU-bound parallelismLimitedYes
Rust library interopNoYes
Message-based workflowsYesPossible but verbose
Simple background taskYesEither

The Send Spec ​

Like Rust, Auto uses the Send spec to indicate that a type can be safely transferred between actors (or threads). The ownership system ensures that values transferred via messages move cleanly from one actor to another.

Most Auto types automatically implement Send. The main exception is types that contain non-thread-safe Rust references accessed via use.rust.

Ownership Transfer in Messages ​

When you send a message containing a value, ownership of that value transfers to the receiving actor:

auto
let data = List.of(1, 2, 3)
worker.send(Process(data))
// data is no longer valid here — ownership was transferred

This prevents the sender from accidentally modifying data that the receiver is now using. The ownership system catches this at compile time.

Cloning Before Sending ​

If you need to keep a copy, clone the data before sending:

auto
let data = List.of(1, 2, 3)
worker.send(Process(data.clone()))
// data is still valid here — we sent a clone
print(data)  // works fine

Concurrency Quick Reference ​

FeatureAuto (Actors)Rust (Threads)
Define unitactor Name { }thread::spawn(|| { })
Send messageactor.send(Msg)tx.send(val)
Request/responseactor.ask(Msg)rx.recv() + tx.send()
Shared stateNot shared — actor owns itArc<Mutex<T>>
Prevent data racesBy design (no sharing)Ownership + type system
Prevent deadlocksBy design (no locks)Careful lock ordering
Create new unitName.init()thread::spawn()
Marker specSendSend + Sync

Summary ​

Auto's actor model provides a fundamentally different approach to concurrency from Rust's thread-based model:

  1. Actors — independent units of computation with private state
  2. Message passing — the only way actors communicate; no shared memory
  3. send and ask — fire-and-forget and request-response patterns
  4. Ownership transfer — messages transfer ownership, preventing data races
  5. No locks — actors process one message at a time, eliminating deadlocks
  6. Rust interop — thread-based concurrency available via use.rust

The actor model trades the flexibility of shared memory for the safety and simplicity of isolated state. For most concurrent applications, this trade-off results in code that is easier to write, easier to understand, and free of entire categories of concurrency bugs.

In the next chapter, we'll explore Auto's async programming model with ~T blueprints and how it relates to Rust's async/await.

Released under the MIT License.