Skip to content
AgileLogics
6 min read

Shared State: Arc & Mutex

Prerequisites: Threads & message passing

Channels move data between threads. Sometimes several threads need to read and update the same data instead. Rust's tools for that are:

  • Arc<T> — "atomically reference counted". The thread-safe version of Rc<T>: cloning it gives another owner of the same value, and the count is updated safely from any thread.
  • Mutex<T> — "mutual exclusion". Only one thread at a time can access the value inside. .lock() waits until the lock is free and returns a guard; the guard derefs to the data and releases the lock automatically when it is dropped.
  • RwLock<T> — a read-write lock: any number of readers at once (.read()), or a single writer (.write()). Useful when reads far outnumber writes.

Together, Arc<Mutex<T>> is the standard way to share mutable state between threads: Arc provides shared ownership, Mutex provides safe mutation.

lock() returns a Result. It is an Err only if another thread panicked while holding the lock (the mutex is then "poisoned"). Calling .unwrap() on it is the common choice, since that situation usually means a bug anyway.

Syntax

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let c = Arc::clone(&counter);                 // another owner for the thread
    thread::spawn(move || {
        let mut n = c.lock().unwrap();            // wait for the lock, get a guard
        *n += 1;
    })                                            // guard dropped here: lock released
    .join()
    .unwrap();
    println!("{}", *counter.lock().unwrap());
}

Example

use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;

fn main() {
    // A shared counter updated by 8 threads.
    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!("counter = {}", *counter.lock().unwrap());

    // A shared map: each thread records its own result.
    let results = Arc::new(Mutex::new(HashMap::new()));
    let handles: Vec<_> = ["apple", "fig", "banana"]
        .into_iter()
        .map(|word| {
            let results = Arc::clone(&results);
            thread::spawn(move || {
                let length = word.len(); // do the work *before* taking the lock
                results.lock().unwrap().insert(word, length);
            })
        })
        .collect();
    for h in handles {
        h.join().unwrap();
    }
    let map = results.lock().unwrap();
    let mut entries: Vec<_> = map.iter().collect();
    entries.sort();
    println!("lengths: {entries:?}");

    // RwLock: many readers, occasional writer.
    let config = Arc::new(RwLock::new(String::from("v1")));
    {
        let reader1 = config.read().unwrap();
        let reader2 = config.read().unwrap(); // two readers at once is fine
        println!("readers see {reader1} and {reader2}");
    }
    config.write().unwrap().push_str("-patched");
    println!("after write: {}", config.read().unwrap());
}

Expected output

counter = 8000
lengths: [("apple", 5), ("banana", 6), ("fig", 3)]
readers see v1 and v1
after write: v1-patched

The counter always ends at exactly 8000: without the mutex, two threads could read the same old value and one update would be lost. Rust doesn't let you write that broken version at all.

Deadlocks

The compiler prevents data races, but not deadlocks — threads waiting on each other forever. Common causes:

  • Locking the same Mutex twice in one thread while the first guard is still alive. std::sync::Mutex is not re-entrant, so the second lock() never returns (or panics — the behavior is unspecified).
  • Two threads taking two locks in opposite orders (thread 1 locks A then B, thread 2 locks B then A). Always acquire multiple locks in the same order.

Keep guards short-lived: copy out what you need, drop the guard (end the block or call drop(guard)), then do slow work.

Common mistakes

  • Sharing an Rc with a thread: error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely. Rc's counter isn't thread-safe; use Arc.
  • Moving the Arc itself into the first thread and then using it again: error[E0382]: use of moved value: `counter` . Clone the Arc for each thread before the move closure.
  • Using Arc<T> alone and trying to modify through it: error[E0596]: cannot borrow data in an `Arc` as mutable. Arc only gives shared access; put a Mutex or RwLock inside.
  • Holding a guard across slow work (file I/O, sleeping, a long computation). Every other thread waits the whole time, and the program may be no faster than a single thread.
  • Reading under one lock and writing under another: let n = *c.lock().unwrap(); *c.lock().unwrap() = n + 1;. Another thread can change the value between the two locks, so an update gets lost. Do the read and the write while holding a single guard.

Practice

Start with Arc::new(Mutex::new(Vec::new())). Spawn five threads with ids 0–4; each pushes id * 10 into the vector. After joining all threads, sort the vector and print it along with its sum.

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

fn main() {
    let values = Arc::new(Mutex::new(Vec::new()));

    let handles: Vec<_> = (0..5)
        .map(|id| {
            let values = Arc::clone(&values);
            thread::spawn(move || {
                values.lock().unwrap().push(id * 10);
            })
        })
        .collect();

    for h in handles {
        h.join().unwrap();
    }

    let mut v = values.lock().unwrap();
    v.sort(); // push order depends on thread scheduling
    let sum: i32 = v.iter().sum();
    println!("{v:?} sum = {sum}");
}
[0, 10, 20, 30, 40] sum = 100