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:
Sendand 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:
- Each actor has its own private state — no other actor can access it
- Actors communicate exclusively by sending asynchronous messages
- 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
| Concept | Rust (Threads) | Auto (Actors) |
|---|---|---|
| Unit of concurrency | Thread | Actor |
| Communication | Channels or shared state | Message passing |
| State sharing | Mutex + Arc | No sharing |
| Data races | Prevented by ownership | Impossible by design |
| Deadlocks | Possible | Impossible (no locks) |
| Error handling | Result types | Message-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
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
moveclosures — 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:
- Creation —
Counter.init()creates a new actor instance - Running — the actor processes messages from its mailbox
- 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)
counter.send(Increment) // fire and forget
let n = counter.ask(GetCount) // waits for responseMessage Passing Patterns
Basic Message Passing
Let's create a more complete example: a logger actor that receives messages and prints them.
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:
Pipeline: Actors in Sequence
Messages can flow through a pipeline of actors, each performing a transformation:
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
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
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:
When to Use Actors vs Threads
| Scenario | Use Actors | Use Threads |
|---|---|---|
| Independent services | Yes | No |
| Shared mutable state | Avoid with actors | Mutex + Arc |
| CPU-bound parallelism | Limited | Yes |
| Rust library interop | No | Yes |
| Message-based workflows | Yes | Possible but verbose |
| Simple background task | Yes | Either |
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:
let data = List.of(1, 2, 3)
worker.send(Process(data))
// data is no longer valid here — ownership was transferredThis 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:
let data = List.of(1, 2, 3)
worker.send(Process(data.clone()))
// data is still valid here — we sent a clone
print(data) // works fineConcurrency Quick Reference
| Feature | Auto (Actors) | Rust (Threads) |
|---|---|---|
| Define unit | actor Name { } | thread::spawn(|| { }) |
| Send message | actor.send(Msg) | tx.send(val) |
| Request/response | actor.ask(Msg) | rx.recv() + tx.send() |
| Shared state | Not shared — actor owns it | Arc<Mutex<T>> |
| Prevent data races | By design (no sharing) | Ownership + type system |
| Prevent deadlocks | By design (no locks) | Careful lock ordering |
| Create new unit | Name.init() | thread::spawn() |
| Marker spec | Send | Send + Sync |
Summary
Auto's actor model provides a fundamentally different approach to concurrency from Rust's thread-based model:
- Actors — independent units of computation with private state
- Message passing — the only way actors communicate; no shared memory
sendandask— fire-and-forget and request-response patterns- Ownership transfer — messages transfer ownership, preventing data races
- No locks — actors process one message at a time, eliminating deadlocks
- 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.