Arc, Mutex and RwLock

Rust · Advanced · 15 min

Any multithreaded server or worker pool shares some state, and these are the standard building blocks.

To share ownership across threads you need `Arc<T>` (atomically reference counted), the thread-safe version of Rc. Cloning an Arc bumps an atomic counter; the data is read-only through it.

To mutate shared data, wrap it in a lock: `Arc<Mutex<T>>`. mutex.lock() blocks until it gets exclusive access and returns a MutexGuard, which derefs to &mut T and unlocks when dropped. There is no separate unlock call to forget; keep critical sections short by limiting the guard's scope with a block or an explicit drop(guard).

lock() returns a Result because of poisoning: if a thread panics while holding the guard, the mutex is marked poisoned, since the data might be half-updated. Most code calls .unwrap() (propagating the panic) or recovers with .unwrap_or_else(|e| e.into_inner()).

`RwLock<T>` allows many readers (read()) or one writer (write()); it helps when reads dominate and are long. For short critical sections a Mutex is often faster.

For simple counters and flags, atomics like AtomicU64 avoid locks entirely: counter.fetch_add(1, Ordering::Relaxed). Memory orderings beyond Relaxed (Acquire/Release, SeqCst) matter when the atomic guards other data; if unsure, use SeqCst or a Mutex.

Deadlocks are still possible: two threads taking locks A then B and B then A. Always acquire multiple locks in a fixed order, and never call unknown code (callbacks) while holding a lock. A guard held across a call that tries to lock the same Mutex again deadlocks the thread with itself; std mutexes are not reentrant.

Alternatives: parking_lot offers smaller, faster locks without poisoning; channels avoid shared state altogether.

Example

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;

fn main() {
    let totals = Arc::new(Mutex::new(Vec::new()));
    let hits = Arc::new(AtomicU64::new(0));
    let config = Arc::new(RwLock::new(String::from("v1")));

    let handles: Vec<_> = (0..4).map(|id| {
        let (totals, hits, config) = (Arc::clone(&totals), Arc::clone(&hits), Arc::clone(&config));
        thread::spawn(move || {
            let version = config.read().unwrap().clone(); // shared read lock
            hits.fetch_add(1, Ordering::Relaxed);         // lock-free counter
            let mut guard = totals.lock().unwrap();       // exclusive access
            guard.push(format!("{id}:{version}"));
        }) // guard dropped here -> unlocked
    }).collect();

    for h in handles { h.join().unwrap(); }
    *config.write().unwrap() = "v2".into();
    let mut done = totals.lock().unwrap().clone();
    done.sort();
    println!("{done:?} hits={}", hits.load(Ordering::Relaxed)); // ["0:v1", "1:v1", "2:v1", "3:v1"] hits=4
}

Remember

  • Arc shares ownership across threads; Mutex/RwLock add mutation
  • Guards unlock on drop; keep them in the smallest possible scope
  • Use atomics for counters and a fixed lock order to avoid deadlocks

Watch out for

  • Locking a Mutex twice on the same thread (for example in a helper called while holding the guard), which deadlocks.

Check yourself

  1. What happens to a std `Mutex` if a thread panics while holding its guard?
  2. Why is `Rc<RefCell<T>>` rejected by `thread::spawn` while `Arc<Mutex<T>>` is accepted?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch16-03-shared-state.html

Arc, Mutex and RwLock · Rust · Topolearn