In this lesson, you will learn how to encapsulate mutable state inside isolated threads—"actors"—that communicate exclusively by passing messages rather than sharing memory.
The Actor Pattern
When multiple threads share memory via Mutexes or RwLocks, you face the constant risk of deadlocks and complex lock-ordering requirements. The Actor Model flips this strategy: instead of sharing the data, you give each piece of state its own dedicated "owner" thread.
An actor consists of three things:
- Private State: Data that is never accessed by other threads.
- A Mailbox: A channel (like an
mpscchannel) used to receive instructions. - The Loop: A continuous task that processes messages and updates the state.
Because the state is entirely contained within the actor’s thread, you don't need locks. The thread processing the loop is the only entity that can mutate the state, effectively turning concurrency into a sequence of serialized message processing events.
Implementation Pattern
Here is how you structure a simple counter actor:
use std::sync::mpsc;
use std::thread;
enum Message {
Increment,
Query(mpsc::Sender<i32>),
}
fn spawn_counter() -> mpsc::Sender<Message> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let mut count = 0; // State is isolated here
while let Ok(msg) = rx.recv() {
match msg {
Message::Increment => count += 1,
Message::Query(reply_tx) => {
let _ = reply_tx.send(count);
}
}
}
});
tx
}
By sending a Message::Increment to the returned tx handle, you request a change without needing to know how the count is stored. If you need a return value, you include a "one-shot" channel in the message, allowing the actor to reply asynchronously. This decouples the requester from the implementation details of the state, ensuring that data races are physically impossible because no memory is shared between threads.
Key Takeaway: The Actor Model replaces shared-memory synchronization with message passing, ensuring that state is only ever mutated by the thread that owns it.