Step 4 of 7
Mutex: one writer at a time
Arc shares data read-only. To change shared data, threads must take turns. A Mutex<T> (short for mutual exclusion) wraps a value so only one thread at a time can get at it:
mutex.lock()waits until no other thread holds the mutex, then returns aMutexGuard: a smart pointer that derefs to the value, so*guard += 1works.- When the guard is dropped (normally at the end of its scope), the mutex unlocks automatically. That's RAII, the same idea as
Dropin module 23: you can't forget to unlock.
With scoped threads, a plain Mutex is enough:
use std::sync::Mutex;
use std::thread;
fn main() {
let visits = Mutex::new(0);
thread::scope(|s| {
for _ in 0..8 {
s.spawn(|| {
for _ in 0..1000 {
let mut count = visits.lock().unwrap();
*count += 1;
}
});
}
});
println!("visits: {}", visits.into_inner().unwrap());
}
visits: 8000
8 threads × 1000 increments, and the answer is 8000 on every run, because each += 1 happens while holding the lock. into_inner() consumes the Mutex once no thread needs it anymore and gives back the value.
Why lock() returns a Result: poisoning
If a thread panics while holding a guard, the value might be half-updated. The mutex is then marked poisoned, and later lock() calls return Err to warn you. .lock().unwrap() (turn that into a panic too) is what almost all code does.
Arc<Mutex>: shared and changeable
Threads from thread::spawn can't borrow, so they share the Mutex through an Arc. Each layer has one job: Arc gives shared ownership, Mutex gives one-at-a-time access for changes. It's the threaded version of Rc<RefCell<T>> from module 29.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut handles = Vec::new();
for id in 1..=3 {
let log = Arc::clone(&log);
handles.push(thread::spawn(move || {
let mut entries = log.lock().unwrap();
entries.push(format!("worker {id} finished"));
}));
}
for handle in handles {
handle.join().unwrap();
}
let mut entries = log.lock().unwrap();
entries.sort();
for entry in entries.iter() {
println!("{entry}");
}
}
worker 1 finished
worker 2 finished
worker 3 finished
The workers push in whatever order they run, so main sorts before printing.
Keep the lock short
Other threads wait for as long as you hold the guard. Do the slow work (parsing, computing, I/O) before locking, then lock, update, and let the guard go. To unlock early, put the guard in a { } block or call drop(guard).
Common mistakes
- Read and write in two separate locks:
let n = *c.lock().unwrap();then*c.lock().unwrap() = n + 1;. Between the two, another thread can update the value, and one update is lost. Do the whole read-change-write under one guard. - Forgetting the
Arcwith spawned threads: theMutexmoves into the first thread, thenerror[E0382]: use of moved value: `counter`. - Holding a guard while doing slow work: every other thread stalls.
- Locking the same mutex twice in one thread: the program freezes. Next step.
Your task
Four threads each make 25 deposits of 10 coins into one shared Account. Share the account with Arc<Mutex<Account>> and spawned threads; each deposit adds 10 to balance and 1 to transactions under one lock. After joining, print:
balance: 1000
transactions: 100
Challenge 2
Five checkout threads each sell one item from the shared stock. Fill in the blank that takes the lock. The program should print left in stock: 15.
Challenge 3
Write fn tally_votes(stations: &[Vec<&str>]) -> BTreeMap<String, u32>. Each inner Vec holds the ballots from one polling station. Count each station in its own scoped thread, all of them updating one shared Mutex<BTreeMap<String, u32>>, and return the final map.
Previous: Arc: shared ownership across threads Next: RwLock, and how deadlocks happen