Mutex, Arc and shared state
A channel moves data from one thread to another. Shared state is the other half of the same problem: one value that every thread reads and some of them write. Rust names it with two types, a Mutex that hands out access one thread at a time and an Arc that lets several threads own the same lock.
This lesson locks a value in one thread, shares a counter between four, shows where a lock ends, and poisons a mutex on purpose. The last section is the compiler refusing a write that never took the lock.
A mutex and its guard
Mutex::new wraps a value and lock returns a guard. The guard dereferences to the value, so reading and writing look like ordinary code, and the mutex is released when the guard drops.
The program increments a counter inside a block and prints it after the block. The braces are there to end the guard before the print, which also locks.
use std::sync::Mutex;
fn main() {
let counter = Mutex::new(0);
{
let mut guard = counter.lock().unwrap();
*guard += 1;
}
println!("{}", counter.lock().unwrap());
}
rustc --edition 2024 mutex.rs
./mutex
1
The unwrap on lock handles poisoning, which the fourth section explains. Printing the guard formats the value inside it, which is why the last line does not need a dereference. The lesson on RefCell is the single-threaded version of the same borrow check.
Arc for the sharing
A Mutex alone cannot cross a thread boundary by reference, because the thread may outlive the function. Arc gives each thread an owner of the same mutex, and the mutex is dropped when the last handle goes.
Four threads increment the same counter twenty-five times each. Every increment takes the lock, so the total is exactly one hundred, and the number does not depend on the interleaving.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
let shared = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..25 {
*shared.lock().unwrap() += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("{}", counter.lock().unwrap());
}
rustc --edition 2024 arc.rs
./arc
100
One hundred is the proof that no update was lost, which a plain i32 in four threads could not promise. The lock is held for one addition per iteration, so the contention is real but the critical section is tiny. The lesson on atomics replaces the lock for exactly this shape.
The lock ends with the guard
The guard is the lock: while it lives the mutex is held, and the statement that creates it holds it to the end of the enclosing block. A second lock from the same thread waits, which is the most common way to write a deadlock by accident.
The program holds a guard and asks for a second lock with try_lock, which returns at once instead of waiting. The request fails, and the value under the first guard is still readable.
use std::sync::Mutex;
fn main() {
let counter = Mutex::new(1);
let guard = counter.lock().unwrap();
match counter.try_lock() {
Ok(_) => println!("second lock"),
Err(_) => println!("locked by another guard"),
}
println!("{}", *guard);
}
rustc --edition 2024 trylock.rs
./trylock
locked by another guard
1
The checked form is the right one when a lock might be held by another path and waiting would stall the program. A lock in the same place would wait forever, and nothing in the program would print the reason.
A poisoned mutex
A thread that panics while holding the guard leaves the protected value in an unknown state, and the mutex records that as poison. Every later lock returns Err(PoisonError), and the program decides whether to trust the value anyway.
The worker increments the counter, panics while holding it and ends. The main thread sees the poison flag and a refused lock, and prints both.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let shared = Arc::clone(&counter);
let worker = thread::spawn(move || {
let mut guard = shared.lock().unwrap();
*guard += 1;
panic!("worker failed while holding the lock");
});
let _ = worker.join();
println!("poisoned: {}", counter.is_poisoned());
match counter.lock() {
Ok(guard) => println!("value {}", *guard),
Err(_) => println!("the lock refused the guard"),
}
}
rustc --edition 2024 poison.rs
./poison
thread '<unnamed>' (924) panicked at poison.rs:10:9:
worker failed while holding the lock
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
poisoned: true
the lock refused the guard
The thread id in the panic line changes on every run. A program that knows the value is still consistent calls into_inner on the error to take the guard anyway, which is how a cache or a counter survives a failed worker. A program that cannot trust it exits, and the lesson on panics shows how the status travels.
A write without the lock
The mutex is a value like any other, so the compiler refuses arithmetic on it. The error is E0368: the addition needs a type that implements the assignment operator, and Mutex<i32> does not.
The program adds one to the mutex itself instead of to the value inside the guard. The compiler points at the operator and at the missing implementation.
use std::sync::{Arc, Mutex};
fn main() {
let counter = Arc::new(Mutex::new(0));
*counter += 1;
println!("{counter:?}");
}
rustc --edition 2024 direct_err.rs
error[E0368]: binary assignment operation `+=` cannot be applied to type `std::sync::Mutex<{integer}>`
--> direct_err.rs:5:5
|
5 | *counter += 1;
| --------^^^^^
| |
| cannot use `+=` on type `std::sync::Mutex<{integer}>`
|
note: `std::sync::Mutex<{integer}>` does not implement `AddAssign<{integer}>`
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/std/src/sync/poison/mutex.rs:227:0
|
= note: `std::sync::Mutex<{integer}>` is defined in another crate
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0368`.
The dereference of the Arc reached the mutex itself, which is what the program asked for by writing *counter. The fix is the lock call, which is the whole point of the type: there is no way to reach the integer without going through the guard. The next lesson waits on a mutex without spinning.
Exercises
- Share a Mutex<Vec<i32>> between two threads and push from both.
- Replace the counter with four threads that each add a different amount and check the total.
- Poison a mutex and take the guard back with into_inner instead of printing the error.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
Mutex::lockreturn?Answer
A guard that releases the lock when it drops
The guard owns the permission to read and write, and dropping it unlocks.
-
Why does the shared counter need an
Arcas well as aMutex?Answer
The threads need several owners of the same mutex
Each thread owns a handle, and the last one drops the mutex.
-
When is the mutex released?
Answer
When the guard is dropped
The guard is the lock, so its scope is the critical section.
-
What happens after a thread panics while holding the lock?
Answer
The mutex is poisoned and later locks return Err
The poison flag marks that the value may be inconsistent.
-
What does this program print?
use std::sync::Mutex; fn main() { let counter = Mutex::new(5); { let mut guard = counter.lock().unwrap(); *guard *= 2; } println!("{}", counter.lock().unwrap()); }Answer
10
The guard writes the doubled value and the block ends the lock before the print.
-
What does this program print?
use std::sync::Mutex; fn main() { let counter = Mutex::new(0); let guard = counter.lock().unwrap(); println!("{}", counter.try_lock().is_err()); println!("{}", *guard); }Answer
true / 0
The first guard holds the mutex, so the try fails and the value is still zero.
-
What does this program print?
use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let shared = Arc::clone(&counter); let worker = thread::spawn(move || { *shared.lock().unwrap() += 3; }); worker.join().unwrap(); println!("{}", counter.lock().unwrap()); }Answer
3
The worker adds three under the lock and the main thread reads it back.
-
What does this program print?
use std::sync::Mutex; fn main() { let counter = Mutex::new(1); { let mut guard = counter.lock().unwrap(); *guard += 4; } println!("{}", counter.try_lock().is_ok()); println!("{}", counter.lock().unwrap()); }Answer
true / 5
The block ends the first lock, so the try succeeds and the value is five.
-
What is the first line of the compiler output for this program?
use std::sync::{Arc, Mutex}; fn main() { let counter = Arc::new(Mutex::new(0)); *counter += 1; println!("{counter:?}"); }Answer
error[E0368]: binary assignment operation
+=cannot be applied to typestd::sync::Mutex<{integer}>The dereference reaches the mutex, and the mutex does not implement the assignment operator.
-
What is the first line of the compiler output for this program?
use std::sync::Mutex; use std::thread; fn main() { let counter = Mutex::new(0); let guard = counter.lock().unwrap(); thread::spawn(move || { println!("{guard}"); }); }Answer
error[E0277]:
std::sync::MutexGuard<'_, i32>cannot be sent between threads safelyThe guard borrows the mutex and holds the lock, so it cannot travel to another thread.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev