Lesson 20 / 25
Threads, Arc, Mutex and Channels
Share safely or pass messages.
Three patterns
thread::spawn with a move closure runs code on a new OS thread. To share mutable state, wrap it in Arc<Mutex<T>>: Arc gives shared ownership across threads, Mutex ensures exclusive access, and the lock guard unlocks automatically when dropped. To pass messages, use channels (mpsc::channel): senders can be cloned, and the receiver iteration ends when all senders are dropped. Scoped threads (thread::scope) can borrow local data because they are guaranteed to finish before the scope ends.
Threads the compiler checks
Rust's type system rules out data races; threads, Arc, Mutex and channels share work safely.
A shared counter, a channel and scoped threads, run
I ran this with Rust 1.99.0 (cargo run, edition 2024, standard library only). Eight threads add 1,000 each to a mutex-protected counter for exactly 8,000; three threads send squares over a channel; a scoped thread borrows data that remains usable afterwards.
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0)); // shared ownership + exclusive access
let mut handles = vec![];
for _ in 0..8 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
*counter.lock().unwrap() += 1;
}
}));
}
for h in handles { h.join().unwrap(); }
println!("counter = {}", *counter.lock().unwrap());
let (tx, rx) = mpsc::channel();
for id in 0..3 {
let tx = tx.clone();
thread::spawn(move || tx.send(id * id).unwrap());
}
drop(tx); // close the original sender
let mut results: Vec<i32> = rx.iter().collect();
results.sort();
println!("squares from threads: {results:?}");
let data = vec![1, 2, 3];
let sum = thread::scope(|s| { // scoped threads may borrow local data
let h = s.spawn(|| data.iter().sum::<i32>());
h.join().unwrap()
});
println!("scoped sum {sum}, data still usable: {data:?}");
}
Output:
counter = 8000 squares from threads: [0, 1, 4] scoped sum 6, data still usable: [1, 2, 3]
Keep lock scopes short
Lock, do the minimal work, and let the guard drop; long-held locks serialise your program.
Quick check: Why wrap a shared counter in Arc<Mutex<i32>>?
- To make it immutable
- Arc shares ownership across threads and Mutex ensures exclusive access
- To avoid using threads
- Because i32 cannot be copied
Answer
Arc shares ownership across threads and Mutex ensures exclusive access — Shared ownership plus synchronisation.