RustArena

Arc<Mutex<T>> in Rust: sharing mutable state between threads

How to share and change data across threads in Rust with Arc and Mutex, how lock guards work, poisoning, RwLock, atomics, and avoiding deadlocks.

When several threads need to change the same data, Rust's usual tools don't work: ownership allows only one owner, &mut allows only one writer, and Rc/RefCell aren't thread-safe. The standard answer is Arc<Mutex<T>>:

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();
    for _ in 0..8 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                *counter.lock().unwrap() += 1;
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("count = {}", *counter.lock().unwrap());
}
count = 8000

How lock() works

mutex.lock() waits until no other thread holds the lock, then returns a MutexGuard. The guard derefs to the data (*guard += 1, guard.push(x)), and unlocks automatically when it's dropped, at the end of the statement or block. You can't forget to unlock, and you can't touch the data without locking: it's inside the mutex.

lock() returns a Result because of poisoning: if a thread panicked while holding the lock, the data might be half-updated, and later lock() calls return Err. Most code calls .unwrap() (propagating the panic) or .unwrap_or_else(|e| e.into_inner()) to accept the data anyway.

Why not Rc<Mutex>?

use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Rc::new(Mutex::new(0));
    let c = Rc::clone(&counter);
    let h = thread::spawn(move || {
        *c.lock().unwrap() += 1;
    });
    h.join().unwrap();
}
error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely

Rc's counter isn't atomic, so two threads cloning it at once could corrupt the count. The compiler knows this through the Send trait. Swap Rc for Arc.

Alternatives

Tool Use when
Mutex<T> general shared mutable state
RwLock<T> many readers, rare writers (read() / write())
AtomicUsize, AtomicBool, ... a single counter or flag: fetch_add(1, Ordering::Relaxed), no lock
channels (mpsc) threads hand results to one owner instead of sharing
thread::scope + &mut chunks each thread works on its own part of the data
OnceLock / LazyLock a global initialized once, then read-only

Common mistakes

Example

use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;

fn main() {
    let orders = vec![("north", 120), ("south", 75), ("north", 30), ("east", 210), ("south", 25), ("east", 5)];

    let totals: Arc<Mutex<BTreeMap<&str, u32>>> = Arc::new(Mutex::new(BTreeMap::new()));
    let processed = Arc::new(AtomicUsize::new(0));
    let config = Arc::new(RwLock::new(10_u32)); // shipping fee, read by all

    let mut handles = Vec::new();
    for chunk in orders.chunks(2) {
        let chunk = chunk.to_vec();
        let totals = Arc::clone(&totals);
        let processed = Arc::clone(&processed);
        let config = Arc::clone(&config);
        handles.push(thread::spawn(move || {
            let fee = *config.read().unwrap();
            for (region, amount) in chunk {
                let with_fee = amount + fee; // compute outside the lock
                *totals.lock().unwrap().entry(region).or_insert(0) += with_fee;
                processed.fetch_add(1, Ordering::Relaxed);
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }

    *config.write().unwrap() = 0; // writers get exclusive access
    println!("processed {} orders", processed.load(Ordering::Relaxed));
    for (region, total) in totals.lock().unwrap().iter() {
        println!("{region:<6} {total}");
    }
    println!("fee is now {}", config.read().unwrap());
}

Output:

processed 6 orders
east   235
north  170
south  120
fee is now 0

Practice it