Why this matters
Message passing covers many designs, but sometimes several threads genuinely need the same data: a counter, a cache, a shared map of results. Shared mutable state is exactly where data races come from: two threads reading and writing the same memory without coordination produce corrupted, unpredictable results. Rust's answer is two types used together: Mutex<T> makes sure only one thread at a time accesses the data, and Arc<T> lets several threads own the mutex. The compiler refuses to let you share data across threads any other way.
The problem: a data race (that Rust will not compile)
use std::thread;
fn main() {
let mut count = 0;
let mut handles = vec![];
for _ in 0..4 {
handles.push(thread::spawn(|| {
count += 1;
}));
}
for h in handles {
h.join().unwrap();
}
println!("{count}");
}
error[E0373]: closure may outlive the current function, but it borrows `count`, which is owned by the current function
--> main.rs:7:36
|
7 | handles.push(thread::spawn(|| {
| ^^ may outlive borrowed value `count`
8 | count += 1;
| ----- `count` is borrowed here
|
note: function requires argument type to outlive `'static`
--> main.rs:7:22
|
7 | handles.push(thread::spawn(|| {
| ______________________^
8 | | count += 1;
9 | | }));
| |__________^
help: to force the closure to take ownership of `count` (and any other referenced variables), use the `move` keyword
|
7 | handles.push(thread::spawn(move || {
| ++++
error[E0499]: cannot borrow `count` as mutable more than once at a time
--> main.rs:7:36
|
7 | handles.push(thread::spawn(|| {
| - ^^ `count` was mutably borrowed here in the previous iteration of the loop
| ______________________|
| |
8 | | count += 1;
| | ----- borrows occur due to use of `count` in closure
9 | | }));
| |__________- argument requires that `count` is borrowed for `'static`
error[E0502]: cannot borrow `count` as immutable because it is also borrowed as mutable
--> main.rs:14:15
|
7 | handles.push(thread::spawn(|| {
| - -- mutable borrow occurs here
| ______________________|
| |
8 | | count += 1;
| | ----- first borrow occurs due to use of `count` in closure
9 | | }));
| |__________- argument requires that `count` is borrowed for `'static`
...
14 | println!("{count}");
| ^^^^^^^ immutable borrow occurs here
|
Beware the first help: here: adding move makes it compile, but wrongly. count is an i32, which is Copy, so each thread would get its own private copy, increment that, and main would print 0. The real problem is sharing, and move does not share.
In C, the equivalent compiles, and count += 1 (load, add, store) from several threads loses updates: two threads load 5, both store 6. Rust rejects it: four closures would need mutable access to one variable at once. We need a way to coordinate access, and a way for several threads to own that coordinator.
Mutex: one at a time
A Mutex<T> (mutual exclusion) wraps a value. To touch the value you must lock the mutex; only one thread can hold the lock at a time, and others wait:
use std::sync::Mutex;
fn main() {
let m = Mutex::new(vec![1, 2, 3]);
{
let mut guard = m.lock().unwrap();
guard.push(4);
println!("inside: {:?}", *guard);
}
m.lock().unwrap().push(5);
println!("after: {:?}", m.lock().unwrap());
}
inside: [1, 2, 3, 4]
after: [1, 2, 3, 4, 5]
lock()blocks until the lock is free and returns aMutexGuard<T>, which derefs to the data (&mut T).- The lock is released when the guard is dropped, normally at the end of its scope. You cannot forget to unlock, and you cannot reach the data without locking: the data is inside the mutex, not merely next to it.
lock()returns aResult: it isErrif another thread panicked while holding the lock (the mutex is "poisoned", since the data may be half-updated).unwrap()is the usual response.
Arc: shared ownership across threads
To give each thread access to the same mutex, the mutex needs several owners: Rc, but thread-safe. That is Arc<T> (atomically reference counted), which updates its count with atomic CPU instructions:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let count = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..8 {
let count = Arc::clone(&count);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
*count.lock().unwrap() += 1;
}
}));
}
for h in handles {
h.join().unwrap();
}
println!("total: {}", *count.lock().unwrap());
println!("owners now: {}", Arc::strong_count(&count));
}
total: 8000
owners now: 1
The pattern is always the same: create Arc::new(Mutex::new(value)), and before spawning each thread, Arc::clone it and move the clone in. Every run prints 8000: no lost updates.
main: count --+
thread 1: count --+--> Arc allocation [ strong: 9 | Mutex [ lock state | 0 ] ]
... |
thread 8: count --+
Read-only data needs no mutex at all: an Arc<Vec<String>> can be shared by many threads, since nobody can modify it.
Keep critical sections short
While one thread holds the lock, all others wanting it wait. Hold the guard only while you actually touch the data:
// bad: lock held during slow work
let mut guard = results.lock().unwrap();
let value = expensive(input);
guard.push(value);
// good: compute first, lock briefly
let value = expensive(input);
results.lock().unwrap().push(value);
A useful trick is to let the guard be a temporary in one statement (*count.lock().unwrap() += 1;) so it is dropped at the semicolon.
Deadlocks
A deadlock happens when threads wait for each other forever: thread 1 holds lock A and waits for B, thread 2 holds B and waits for A. Rust does not prevent deadlocks (they are not memory-unsafe). Avoid them by:
- holding at most one lock at a time when you can,
- always acquiring multiple locks in the same global order,
- never calling unknown code (callbacks) while holding a lock.
Locking the same Mutex twice in one thread also deadlocks (or panics): let a = m.lock(); let b = m.lock();.
RwLock: many readers or one writer
RwLock<T> allows any number of simultaneous readers (read()) or one writer (write()): the borrowing rule, enforced at run time across threads. It pays off for data that is read often and written rarely, like configuration.
Common mistakes
- Mutating shared data without a Mutex: E0499/E0373/E0596, depending on the shape. Wrap it in
Arc<Mutex<T>>. - Using
Rcinstead ofArc: E0277 "Rc<...>cannot be sent between threads safely" (next lesson). - Forgetting
Arc::clonebeforemove: the first thread's closure takes the only Arc, and the next iteration gets E0382. - Holding a guard across slow work or across
join: other threads stall, possibly forever. - Locking in a loop body for every tiny step when you could lock once per batch: correct but slow.
Recap
- A data race is unsynchronized access by several threads with at least one writer; Rust rejects such code.
Mutex<T>holds data that can only be reached throughlock(), which returns a guard; dropping the guard unlocks.Arc<T>is a thread-safeRc;Arc<Mutex<T>>is the standard shared, mutable state.- Keep critical sections short; avoid deadlocks by lock ordering and holding few locks.
RwLockallows many readers or one writer.
Going further: read the Rust Book chapter 16.3. Then replace the counter's Mutex<u64> with std::sync::atomic::AtomicU64 and fetch_add, covered briefly in the next lesson.