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
42: Shared State with Mutex and Arc
Up until now, we've talked about moving data into threads or using channels to pass messages. That works great when data has a clear owner. But in the real world, you often hit a wall where multiple threads need to read and write to the same piece of state simultaneously. For today, let's build a simple Concurrent Request Tracker—something that simulates a web server counting how many requests it's handling across different threads.
The naive approach and the ownership wall
My first instinct is usually to just create a counter and pass it to a few threads. I want a simple integer that gets incremented every time a "request" completes. Here is where I'd start:
use std::thread;
fn main() {
let mut count = 0;
let mut handles = vec![];
for i in 0..10 {
let handle = thread::spawn(move || {
count += 1;
println!("Thread {} incremented count to {}", i, count);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
If you try to compile this, the compiler will scream at you. Why? Because of that move keyword. The first thread takes ownership of count, and the second thread finds that count is already gone. Even if I tried to use a reference, the compiler would complain that it can't guarantee the reference lives long enough. We need a way for multiple threads to "own" the counter at the same time.
The mistake: Falling for the Rc trap
Since I know Rc (Reference Counted) pointers allow multiple owners, I might try to wrap the counter in an Rc. I've done this plenty of times in single-threaded UI code, so it's a natural reflex.
use std::rc::Rc;
use std::thread;
fn main() {
let count = Rc::new(0);
// ... imagine trying to spawn threads here ...
}
Wait. The compiler catches me immediately. It tells me that Rc<i32> cannot be sent between threads safely. This is because Rc uses a non-atomic reference counter. If two threads tried to increment the reference count at the exact same time, they could create a data race, leading to memory leaks or crashes. This is a classic Rust "gotcha"—Rc is for single-threaded sharing; for multi-threaded sharing, we need Arc (Atomic Reference Counted).
Combining Arc and Mutex for shared mutability
Switching to Arc solves the ownership problem, but it introduces a new one: Arc only gives us immutable access to the data it holds. You can't just change a value inside an Arc. To get around this, we use Interior Mutability. We wrap the data in a Mutex (Mutual Exclusion) and then wrap that Mutex in an Arc.
Think of the Arc as the "handle" that lets multiple threads hold onto the box, and the Mutex as the "lock" on the box that ensures only one thread can reach inside and change the value at a time.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc allows multiple owners, Mutex allows safe mutation
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for i in 0..10 {
// We clone the Arc, not the data itself.
// This just increments the atomic reference count.
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// lock() blocks the thread until the mutex is available.
// It returns a Result, so we unwrap it.
let mut num = counter_clone.lock().unwrap();
*num += 1;
println!("Thread {} incremented count to {}", i, *num);
// The lock is automatically released when 'num' goes out of scope.
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap());
}
A few notes on the mechanics
- The Clone: Notice I used
Arc::clone(&counter)instead ofcounter.clone(). They do the same thing, but using the explicitArc::clonemakes it clear to anyone reading your code that you are cloning the pointer, not the actual data inside. - The Guard: When you call
.lock().unwrap(), you aren't getting the integer back. You're getting aMutexGuard. This is a smart pointer that implementsDerefMut, which is why we use*num += 1to reach the actual value. - Poisoning: You might wonder why we
.unwrap()the lock. If a thread panics while holding a lock, the Mutex becomes "poisoned." Most of the time, if a thread panicked while holding your state, the state is corrupted anyway, so unwrapping and crashing the whole program is often the safest bet.
📋 Practical Task
Build a Thread-Safe Shared Configuration Cache
In a real application, you often have a configuration object that needs to be updated occasionally (e.g., updating a log level or a feature flag) while being read by many different worker threads.
Your Task: Create a program that manages a shared Config struct.
- Define a struct
Configwith a fieldlog_level: String. - Wrap this
Configin anArc<Mutex<...>>. - Spawn 5 "worker" threads that periodically print the current
log_level. - Spawn 1 "admin" thread that changes the
log_levelfrom"INFO"to"DEBUG"after a short delay (usestd::thread::sleep). - Ensure the program waits for all threads to complete before exiting.
There are no comments for now.