We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Fearless Concurrency: Threads, Channels, Shared State step 15 of 24
Deadlock by lock ordering: the guarantee Rust does not give you
Settle a batch of account-to-account transfers in parallel and return the final balances.
pub fn settle_transfers(
balances: Vec<i64>,
transfers: Vec<(usize, usize, i64)>,
workers: usize,
) -> Vec<i64>
Accounts are a Vec<Mutex<i64>> inside an Arc — one lock per account, not
one lock for the ledger. Each transfer (from, to, amount) must hold both
accounts’ locks while it moves the money, because a half-applied transfer is
exactly the invariant violation locks exist to prevent.
Split the transfers across workers threads. Balances may go negative;
overdrafts are allowed. Treat workers == 0 as 1. The grader passes tens of
thousands of transfers in some cases.
This problem will hang before it works. That is the curriculum.
Rust eliminates data races. It does not eliminate deadlocks.
This is the honest centre of “fearless concurrency”. The Send/Sync system
makes data races — two unsynchronised accesses, one of them a write —
impossible in safe code, completely, at compile time. That is a genuine and
rare achievement.
Deadlock is a different failure. Every individual access here is perfectly synchronised. Every lock is released correctly by its guard. Nothing is racing. The program is simply waiting, forever, and no type system in any production language catches it.
Watch it happen
The starter locks from, then to. Now run two threads:
thread A: transfer 0 -> 1 thread B: transfer 1 -> 0
lock(account 0) OK lock(account 1) OK
lock(account 1) ...waits lock(account 0) ...waits
A holds 0 and wants 1. B holds 1 and wants 0. Neither will ever release what it has, because releasing happens after acquiring. This is the textbook circular wait, and with 10,000 crossing transfers it happens within milliseconds.
The fix: a global lock order
Deadlock needs a cycle in the “who waits for whom” graph. Remove the possibility of a cycle and you remove deadlock — and the way to do that is to impose a total order on the locks and require that every thread acquires them in that order.
Here the accounts are indexed, so the order is free: always lock the lower index first.
let (low, high) = if from < to { (from, to) } else { (to, from) };
let mut lo = accounts[low].lock()?;
let mut hi = accounts[high].lock()?;
Both threads in the example now try lock(0) first. One wins, does its work,
releases; the other proceeds. No cycle is constructible, because “waits for”
now always points from a lower index to a higher one and a cycle would have to
come back down.
Then be careful applying the amount: low is not necessarily from. Which
guard gets debited depends on the original direction, not on the lock order.
The sting in the tail
There is a hidden test with from == to.
A correctly-ordered solution self-deadlocks on it, because low == high
and the thread locks the same mutex twice. std::sync::Mutex is not
reentrant — the docs say the behaviour is unspecified, and in practice the
thread waits for a lock it is already holding, forever.
A self-transfer is a no-op, so skip it. But notice what happened: you fixed the deadlock in general and introduced a new one in a degenerate case that a quick test would never cover. That is very much what real concurrency debugging is like.
No lint catches any of this
Nothing in rustc or clippy has an opinion about lock ordering. There is no
analysis, no warning, no #[lock_order] attribute. This problem’s starter
compiles clean and passes clippy -D warnings, and then hangs.
Nor is it a std limitation you can buy your way out of. parking_lot and
crossbeam do not fix lock ordering either — parking_lot ships an optional
deadlock_detection feature that will tell you after the fact, which is
genuinely useful and is still not prevention. This is a class of bug where
tooling does not help you and discipline does. Track 15 closes with an
honest inventory of what this grader can and cannot see; this problem is the
first entry.
If your submission times out
It is wedged. Ask: do two of my threads ever want the same two locks in opposite orders? And: can one thread ever ask for the same lock twice?
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.