We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Fearless Concurrency: Threads, Channels, Shared State step 11 of 24
Mutex and MutexGuard: you cannot forget to unlock
Add up a list of increments through one shared counter, and recover the final value without locking.
pub fn concurrent_counter(increments: Vec<u64>, workers: usize) -> u64
Share an Arc<Mutex<u64>>, give each worker a chunk of the increments, join
everything, and return the total. Treat workers == 0 as 1; empty input gives
0. Some totals here exceed u32::MAX, so the type matters.
The design insight: the data lives inside the lock
In C, a mutex and the data it protects are two separate variables joined only
by a comment and your good intentions. In Java, synchronized guards a block,
and nothing stops another method touching the field without it. In both, the
relationship between “this lock” and “that data” exists only in your head.
Rust’s Mutex<T> contains the T. There is no way to name the protected
value without going through lock(), because the value is not a separate
variable at all. That single design decision eliminates a whole class of bug
by construction:
let counter = Mutex::new(0u64);
// there is no `counter` integer anywhere. only the mutex.
*counter.lock().unwrap() += 1;
RAII: you cannot forget to unlock
lock() returns a MutexGuard<'_, T>, not a T. The guard:
-
derefs to
&Tand&mut T, so it acts like the value; -
unlocks in its
Dropimpl — when the guard goes out of scope, the lock is released. There is nounlock()method to forget, and an earlyreturnor a panic in the middle still releases it, because unwinding runs destructors.
So the two classic C bugs — forgetting to unlock, and returning early past the unlock — are not expressible. The bug that is still expressible is holding the guard longer than you meant to, which is the next problem.
Why lock() returns a Result
Because a mutex can be poisoned: if a thread panics while holding the
guard, the data may be halfway through an update, and every later lock()
returns Err. That is why every Mutex example you have ever seen is
peppered with .unwrap(). It is not sloppiness, and there is a problem later
in this track on handling it properly.
Getting the value out at the end — and the starter’s error
The starter finishes with counter.into_inner() and does not compile:
error[E0507]: cannot move out of an `Arc`
|
| counter.into_inner().expect("mutex poisoned")
| ^^^^^^^ ------------ value moved due to this method call
| |
| move occurs because value has type `Mutex<u64>`, which does not
| implement the `Copy` trait
Method resolution walked through Arc‘s Deref to find Mutex::into_inner,
which takes self by value — and you cannot move a value out from behind
a shared pointer. Several handles might exist; moving out would leave the
others dangling.
The fix is to unwrap the layers in order. Arc::into_inner(arc) returns
Option<T>: Some if this was the last handle, None otherwise. Every
worker has been joined, so every clone has been dropped, so it is Some —
deterministically, for the same reason strong_count was 1 two problems ago.
Arc::into_inner(counter)
.expect("last handle")
.into_inner() // Mutex::into_inner
.expect("not poisoned")
Both into_inners take self. Owning the container is already proof that
no one else can be looking, so neither has to lock or synchronise anything.
Mutex::get_mut(&mut self) is the same argument through a &mut.
::: question A colleague writes fn bump(m: &mut Mutex<u64>, by: u64) { *m.lock().unwrap() += by; } and clippy rejects it. Why?
clippy::mut_mutex_lock, and it is a good lint:
warning: calling `&mut Mutex::lock` unnecessarily
= help: change this to: `m.get_mut().unwrap()`
The &mut Mutex<u64> parameter already proves exclusive access — that is
what &mut means, and no other reference to this mutex can exist while it is
alive. Locking is therefore pure overhead: an atomic operation, possibly a
syscall, to acquire something you provably already have.
get_mut() skips it. The deeper point is that &mut and “the lock is held”
are two ways of expressing the same guarantee, and the compiler’s version is
free.
:::
Two facts worth carrying
Mutex::new is a const fn (since 1.63), so a global lock needs no
lazy_static, no OnceLock, nothing:
static LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
And Mutex is not reentrant. Locking it twice on the same thread is
explicitly unspecified behaviour, and on the common platforms it simply
deadlocks — a thread waiting for a lock it is already holding, forever. std
offers no reentrant mutex. If you want one, you are meant to restructure.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.