Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
140: Practice Exercise: Building a Simple Actor System
When you first start building actor systems in Rust, the instinct is often to reach for Arc<Mutex<T>> for everything. You want your actors to be accessible, and you want them to be able to change their internal state. But this is exactly where most people trip up. I've seen a lot of developers build a "system" that is actually just a collection of shared-state objects that deadlock the moment you add a second actor.
Take a look at this snippet. This is a classic "first attempt" at a simple counter actor.
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
enum Message {
Increment,
GetCount(mpsc::Sender<u32>),
}
struct CounterActor {
count: u32,
}
fn main() {
let (tx, rx) = mpsc::channel();
let actor = Arc::new(Mutex::new(CounterActor { count: 0 }));
let actor_clone = Arc::clone(&actor);
thread::spawn(move || {
while let Ok(msg) = rx.recv() {
let mut state = actor_clone.lock().unwrap();
match msg {
Message::Increment => state.count += 1,
Message::GetCount(reply_tx) => {
reply_tx.send(state.count).unwrap();
}
}
}
});
tx.send(Message::Increment).unwrap();
// Imagine more complex logic here...
}
The Mutex Guard Bottleneck
At first glance, this looks fine. It compiles. But there's a fundamental architectural flaw here: we are wrapping the actor's state in a Mutex and then managing the message loop outside of that state. In a real system, if the GetCount logic became more complex—perhaps needing to query another actor—this code would deadlock instantly. You're holding a lock on the state while waiting for external I/O or other actors to respond.
More importantly, you're fighting the "Actor" philosophy. An actor shouldn't be a piece of shared state that is modified by a loop; the actor is the loop and the state combined. By using Arc<Mutex<T>>, you're treating the actor like a global variable with a fancy wrapper.
Owning State via Message Passing
The fix is to move the state inside the thread's closure. We don't need Arc or Mutex at all for the internal state because the thread that processes the messages is the sole owner of that data. This eliminates locking overhead and removes the possibility of deadlocks within the actor itself.
use std::sync::mpsc;
use std::thread;
enum Message {
Increment,
GetCount(mpsc::Sender<u32>),
}
struct CounterActor {
count: u32,
}
impl CounterActor {
fn new() -> mpsc::Sender<Message> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let mut state = CounterActor { count: 0 };
while let Ok(msg) = rx.recv() {
match msg {
Message::Increment => state.count += 1,
Message::GetCount(reply_tx) => {
let _ = reply_tx.send(state.count);
}
}
}
});
tx
}
}
fn main() {
let counter = CounterActor::new();
counter.send(Message::Increment).unwrap();
let (reply_tx, reply_rx) = mpsc::channel();
counter.send(Message::GetCount(reply_tx)).unwrap();
println!("Count: {}", reply_rx.recv().unwrap());
}
Notice how much cleaner this is. The CounterActor doesn't exist as a struct in the main thread; instead, we hold a Sender. This is the core of the actor model: you don't call methods on an object; you send a message to an address. The state is private, encapsulated, and modified sequentially by the message loop. No locks, no contention, just a clean stream of events.
Handling Actor Shutdown
One thing I always tell my juniors: don't forget about the "poison pill." In the example above, the actor thread runs forever until the Sender is dropped. In a production system, you want a way to tell an actor to shut down gracefully so it can save its state or close database connections.
The simplest way to do this is to add a Shutdown variant to your message enum. When the actor receives Shutdown, it simply breaks the while let loop. Because the state is owned by the closure, it will be dropped naturally as the thread exits. This is far more predictable than just killing the thread or relying on the channel to close.
📋 Practical Task
Exercise: Building a Concurrent Log-Aggregator Actor System
Your task is to build a system where multiple "Worker" actors send log messages to a single "Aggregator" actor. The Aggregator should maintain a count of how many logs of each level (Info, Warning, Error) it has received.
- The Message Types: Create an enum
LogMessagethat can be aLog(LogLevel, String)or aGetReport(mpsc::Sender<HashMap<LogLevel, u32>>). - The Aggregator: Implement an Aggregator actor that owns a
HashMap<LogLevel, u32>. It should process logs by incrementing the count for that level and respond toGetReportrequests with the current map. - The Workers: Spawn three separate threads. Each thread should act as a worker, sending at least 5 random log messages (varying levels) to the Aggregator.
- The Shutdown: Implement a
Shutdownmessage. Once the workers are finished, send the shutdown signal to the Aggregator and ensure the program exits cleanly.
Constraint: Do not use Arc<Mutex<T>> to share the log counts. All state updates must happen via message passing within the Aggregator's own thread loop.
There are no comments for now.