RustArena

Step 5 of 7

RwLock, and how deadlocks happen

A Mutex lets one thread in at a time, even if all of them only want to read. RwLock<T> (reader-writer lock) has two kinds of lock:

It's the runtime version of the borrowing rule you know: many readers or one writer. It fits data that's read constantly and changed rarely, like a config or a routing table.

use std::sync::RwLock;
use std::thread;

struct Config {
    max_players: u32,
    motd: String,
}

fn main() {
    let config = RwLock::new(Config { max_players: 8, motd: String::from("Welcome!") });

    let (players, motd) = thread::scope(|s| {
        let a = s.spawn(|| config.read().unwrap().max_players);
        let b = s.spawn(|| config.read().unwrap().motd.clone());
        (a.join().unwrap(), b.join().unwrap())
    });
    println!("readers saw: {players} players, {motd:?}");

    {
        let mut cfg = config.write().unwrap();
        cfg.max_players = 16;
        cfg.motd = String::from("Season 2 is live");
    }

    let cfg = config.read().unwrap();
    println!("after update: {} players, {:?}", cfg.max_players, cfg.motd);
}
readers saw: 8 players, "Welcome!"
after update: 16 players, "Season 2 is live"

The block around the write guard drops it before the final read(). An RwLock does more bookkeeping than a Mutex, so for data that's written often, or locked only very briefly, a Mutex is usually just as fast. Measure before switching.

Deadlocks

A deadlock is when threads wait for each other forever. The program doesn't crash and doesn't print an error: it just freezes. Rust prevents data races at compile time, but not deadlocks: they're a logic bug, like an infinite loop. The two classic causes:

1. Locking the same mutex twice in one thread. std's Mutex isn't reentrant: a second lock() in the same thread waits for the first guard to be dropped, which never happens. It usually hides behind a helper function:

fn total(stock: &Mutex<Vec<u32>>) -> u32 {
    stock.lock().unwrap().iter().sum()
}

let mut items = stock.lock().unwrap();
items.push(5);
println!("{}", total(&stock)); // freezes: `items` still holds the lock

The same goes for an RwLock: holding a read guard and asking for write() in the same thread never returns.

2. Two locks taken in opposite orders. Thread A locks alice, then wants bob. Thread B locks bob, then wants alice. Each holds what the other is waiting for. The fix is lock ordering: whenever code needs several locks, it takes them in one agreed order (for example, by account id), everywhere.

Habits that prevent deadlocks

Your task

This program freezes (on this site, the time limit runs out) because report locks the inventory while main still holds a guard on it. Make main release its guard before calling report, so the program prints items: 4, total units: 26.

Challenge 2

Write fn transfer(from: &Account, to: &Account, amount: i64) that moves amount from one balance to the other while holding both locks. To avoid deadlocks, always lock the account with the smaller id first, whichever direction the money goes. A transfer from an account to itself must do nothing (locking the same mutex twice would freeze). One hidden test runs thousands of transfers in opposite directions at the same time.

Challenge 3

A price table is read by many threads and updated now and then. Write two functions for a RwLock<BTreeMap<String, u32>>:

Previous: Mutex: one writer at a time Next: Atomics: lock-free counters and flags