RustArena

Step 4 of 7

Mutex: one writer at a time

Arc shares data read-only. To change shared data, threads must take turns. A Mutex<T> (short for mutual exclusion) wraps a value so only one thread at a time can get at it:

With scoped threads, a plain Mutex is enough:

use std::sync::Mutex;
use std::thread;

fn main() {
    let visits = Mutex::new(0);
    thread::scope(|s| {
        for _ in 0..8 {
            s.spawn(|| {
                for _ in 0..1000 {
                    let mut count = visits.lock().unwrap();
                    *count += 1;
                }
            });
        }
    });
    println!("visits: {}", visits.into_inner().unwrap());
}
visits: 8000

8 threads × 1000 increments, and the answer is 8000 on every run, because each += 1 happens while holding the lock. into_inner() consumes the Mutex once no thread needs it anymore and gives back the value.

Why lock() returns a Result: poisoning

If a thread panics while holding a guard, the value might be half-updated. The mutex is then marked poisoned, and later lock() calls return Err to warn you. .lock().unwrap() (turn that into a panic too) is what almost all code does.

Arc<Mutex>: shared and changeable

Threads from thread::spawn can't borrow, so they share the Mutex through an Arc. Each layer has one job: Arc gives shared ownership, Mutex gives one-at-a-time access for changes. It's the threaded version of Rc<RefCell<T>> from module 29.

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let log = Arc::new(Mutex::new(Vec::new()));
    let mut handles = Vec::new();
    for id in 1..=3 {
        let log = Arc::clone(&log);
        handles.push(thread::spawn(move || {
            let mut entries = log.lock().unwrap();
            entries.push(format!("worker {id} finished"));
        }));
    }
    for handle in handles {
        handle.join().unwrap();
    }
    let mut entries = log.lock().unwrap();
    entries.sort();
    for entry in entries.iter() {
        println!("{entry}");
    }
}
worker 1 finished
worker 2 finished
worker 3 finished

The workers push in whatever order they run, so main sorts before printing.

Keep the lock short

Other threads wait for as long as you hold the guard. Do the slow work (parsing, computing, I/O) before locking, then lock, update, and let the guard go. To unlock early, put the guard in a { } block or call drop(guard).

Common mistakes

Your task

Four threads each make 25 deposits of 10 coins into one shared Account. Share the account with Arc<Mutex<Account>> and spawned threads; each deposit adds 10 to balance and 1 to transactions under one lock. After joining, print:

balance: 1000
transactions: 100

Challenge 2

Five checkout threads each sell one item from the shared stock. Fill in the blank that takes the lock. The program should print left in stock: 15.

Challenge 3

Write fn tally_votes(stations: &[Vec<&str>]) -> BTreeMap<String, u32>. Each inner Vec holds the ballots from one polling station. Count each station in its own scoped thread, all of them updating one shared Mutex<BTreeMap<String, u32>>, and return the final map.

Previous: Arc: shared ownership across threads Next: RwLock, and how deadlocks happen