We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Fearless Concurrency: Threads, Channels, Shared State step 12 of 24
Guard lifetimes: still holding the lock, still not meaning to
A shared RwLock<Option<u64>> is lazily initialised and then accumulated
into, by several workers at once.
pub fn get_or_init_shared(initial: Option<u64>, ops: Vec<u64>, workers: usize) -> u64
The cell starts as initial. Each op x does one of two things:
-
if the cell is still
None, initialise it tox; -
otherwise, add
xto what is there.
Return the final contents, with None reported as 0. Treat workers == 0
as 1.
Notice that both branches produce the same total —
initial.unwrap_or(0) + ops.iter().sum::<u64>() — regardless of which thread
gets there first. That is deliberate: it makes the answer schedule-independent
so it can be tested at all, while the structure you have to write is the
genuinely racy read-then-write one.
Deadlock in Rust is a guard-lifetime problem
Rust eliminates data races completely. It eliminates deadlocks not at all — and real Rust deadlocks are almost never the textbook “two threads, two locks”. They are one thread, one lock, held longer than the author believed. Guard lifetime is Rust’s deadlock story, which is why it gets a problem of its own.
A MutexGuard or RwLockReadGuard releases the lock in its destructor. So
the question “when is this lock released?” is exactly the question “when does
this temporary die?” — and temporaries have rules most people have never had
to think about precisely.
The starter’s line, and why it is denied
let _ = shared.read().expect("lock poisoned");
error: non-binding let on a synchronization lock
= help: consider immediately dropping the value using `drop(..)`
after the `let` statement
= note: `#[deny(let_underscore_lock)]` on by default
This is deny-by-default in rustc itself — a hard error, not a lint you could argue about. It is one of very few lints promoted to that status, and the reason is that the two lines below look identical and behave in opposite ways:
let _ = m.lock().unwrap(); // guard dropped IMMEDIATELY. lock not held.
let _g = m.lock().unwrap(); // guard lives to end of scope. lock held.
_ is not a binding, it is a wildcard pattern: nothing is bound, so the
temporary dies at the end of the statement. _g is a binding, so the
guard lives to the end of the block. One character, and in a “lock this
around the critical section” context the wildcard version silently protects
nothing at all.
Which is exactly why it is denied rather than warned: the compiler cannot
tell which you meant, and both possible intentions are better written another
way. If you want the lock held, name the guard. If you want it released now,
do not take it, or drop(...) it explicitly.
Now the trap the lint is protecting you from
Suppose you “fix” the error by naming the guard:
let _g = shared.read().expect("lock poisoned"); // read lock held
let mut w = shared.write().expect("lock poisoned"); // ...and now ask for write
That self-deadlocks. A writer must wait for all readers to leave, and this
thread is one of the readers, and it is currently blocked waiting to become a
writer. It will wait forever. std::sync locks are not reentrant and make no
promise here — the docs say behaviour when a thread tries to acquire a lock it
already holds is unspecified, and in practice you hang.
So the fix is structural: the read guard must be gone before you ask for the write lock. In the reference solution the read is a temporary of its own statement:
let needs_init = shared.read().expect("lock poisoned").is_none();
// guard died at the semicolon. only the bool survives.
let mut w = shared.write().expect("lock poisoned");
The bug hiding in that fix
Between the read guard dropping and the write guard being acquired, another
thread can change the value. needs_init is not a fact about the present,
it is a fact about the past.
So the write branch re-checks under the write lock:
if needs_init && w.is_none() { *w = Some(x); } else { ... }
This is double-checked locking done correctly, and the correction is the whole point: a cheap read-lock check as a fast path, and an authoritative re-check under the lock that actually decides. Skipping the second check is one of the most common concurrency bugs in any language.
Two more traps, one of which the 2024 edition deleted
match scrutinees still hold their temporaries. This holds the lock for
the entire body:
match *m.lock().unwrap() { ... } // guard alive until the match ends
If any arm tries to lock m again, you hang.
if let used to do the same, and no longer does. Before the 2024 edition,
if let Some(x) = *m.lock().unwrap() { .. } else { .. } kept the guard alive
through the else branch, so re-locking there deadlocked —
clippy::if_let_mutex exists for exactly that. Edition 2024 rescoped if let temporaries and fixed it, and this grader compiles with
--edition 2024, so the same program that deadlocked under 2021 runs
correctly here. Worth knowing as history, because you will read code and
advice written before the change — and because it makes the edition system
concretely valuable rather than an abstraction.
Note the asymmetry: if let was rescoped, match was not. That gap is now
the live trap.
And: never hold a guard across handle.join(). If the thread you are
waiting on needs the same lock, neither of you moves again.
If your submission hangs
A hang is a deadlock, and the grader can only report it as a timeout, which
is a terrible error message. Go looking for a guard that is still alive: a
named guard, a match scrutinee, a lock taken inside a branch of an
expression whose value has not been consumed yet.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.