22.3Shared state: Mutex and Arc

Last updated June 13, 2026

Channels (lesson 22.2) move data so it's never shared. But some problems are inherently about shared data: a counter many threads increment, a cache they all read and update, one configuration they all consult. For these you need multiple threads touching the same memory, and that's where concurrency is genuinely hard. Rust makes it safe with two cooperating tools: a Mutex to control access, and an Arc to share ownership. Neither alone is enough; together they're the standard pattern.

Mutex: one thread at a time

A mutex (short for "mutual exclusion") is a lock around a piece of data. The rule it enforces: to touch the data, a thread must first acquire the lock, and only one thread can hold the lock at a time. Everyone else waits. This guarantees that at any instant exactly one thread is accessing the data, no two threads ever read-and-write it at once, which is precisely what a data race is.

In Rust, Mutex<T> wraps the data it protects, you can't get at the value without going through the lock. Here it is single-threaded, just to see the API:

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }   // lock released here when `num` goes out of scope

    println!("{m:?}");
}
Mutex { data: 6, poisoned: false, .. }

m.lock() acquires the lock, blocking until it's available, and returns a guard (a MutexGuard) that you use like a mutable reference to the inner value: *num = 6 changes the data through it. The lock() returns a Result (it fails if another thread panicked while holding the lock, "poisoning" it), so we unwrap. The crucial part is the release: when the guard num goes out of scope, the lock is automatically released by its Drop (chapter 21, RAII at work). You never manually unlock; scope does it, which means you can't forget to unlock, the single most common mutex bug in other languages.

Notice that Mutex gave us mutation (*num = 6) even though m isn't mut. That's interior mutability (lesson 21.6) again: Mutex is the thread-safe RefCell, enforcing the borrow rules with a runtime lock instead of a runtime counter.

Sharing the mutex across threads: enter Arc

Now the real goal: many threads sharing one mutex. They all need to own the mutex (it has to outlive each of them), which is shared ownership, the job of Rc from chapter 21. But Rc is single-threaded only (its count isn't safe to update concurrently), so the compiler won't let it cross a thread boundary. Its thread-safe sibling is Arc<T>: "atomically reference counted." Same shared-ownership idea as Rc, but with a counter that's safe to update from multiple threads at once.

The canonical example, ten threads each incrementing a shared counter:

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

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

    println!("result: {}", *counter.lock().unwrap());
}
result: 10

Read the shape, because it's the pattern you'll reuse constantly. Arc::new(Mutex::new(0)) wraps the counter in a mutex (for safe access) inside an Arc (for shared ownership). The loop spawns ten threads; each gets its own Arc::clone (a cheap new handle to the same counter, lesson 21.5) and moves it into the thread. Inside, each thread locks, increments, and the lock releases at the end of the closure. We collect the handles and join them all so main waits for every thread. The result is reliably 10: the mutex guarantees the ten increments don't trample each other.

Arc<Mutex<T>> is Rc<RefCell<T>> for threads

The pairing mirrors chapter 21 exactly. Single-threaded shared mutable state was Rc<RefCell<T>>: Rc for many owners, RefCell for runtime-checked mutation. Across threads it becomes Arc<Mutex<T>>: Arc for many owners (thread-safe counting), Mutex for one-at-a-time mutation (locking). Same two-axis decision, "how many owners?" and "how is mutation controlled?", with the thread-safe versions of each tool. If you understood Rc<RefCell<T>>, you already understand Arc<Mutex<T>>; only the enforcement mechanism changed from a counter and a borrow flag to an atomic counter and a lock.

Why two types instead of one

It's reasonable to ask why Rust splits this into Arc and Mutex rather than one combined type. Because they solve independent problems. Arc answers "who owns this?", many threads, freed when the last releases it, but Arc alone gives only shared read access, just like Rc. Mutex answers "how do we mutate it safely?", lock, change, unlock, but Mutex alone is a single value with no way to share it among threads. You need both because the problem has both halves: shared ownership and safe mutation. Keeping them separate also means you only pay for what you use, shared immutable data needs just Arc, and a mutex owned by one thread needs no Arc.

Locks can deadlock

Rust prevents data races, but it does not prevent deadlock: two threads each holding a lock the other needs, both waiting forever. If thread A locks mutex 1 then waits for mutex 2, while thread B locks mutex 2 then waits for mutex 1, neither can proceed. This compiles fine, it's not a memory-safety violation, but the program hangs. The defenses are discipline, not the compiler: acquire multiple locks in a consistent order everywhere, hold locks for as short a time as possible, and prefer message passing when you can. Fearless concurrency means no data races, not no logic bugs.

Quiz time

Question #1

What does a Mutex guarantee, and how is the lock released?

Show solution

A Mutex guarantees that only one thread at a time can access the data it wraps: a thread must call .lock() (blocking until the lock is free) to get a guard it uses like a mutable reference, and all other threads wait. The lock is released automatically when the guard goes out of scope (its Drop runs, chapter-21 RAII), so you can't forget to unlock. This one-at-a-time access is what prevents data races on the shared data.

Question #2

Why do you need Arc and Mutex together to share mutable data across threads? What does each provide?

Show solution

Arc provides thread-safe shared ownership, multiple threads can each own a handle to the same value (with an atomic reference count safe to update concurrently), and the value lives until the last handle drops. Mutex provides safe mutation, exclusive, one-at-a-time access via locking. Neither alone suffices: Arc alone gives only shared read access; a bare Mutex can't be shared among threads. Arc<Mutex<T>> combines shared ownership with safe mutation.

Question #3

Why is Arc used here instead of Rc?

Show solution

Rc's reference count is not safe to update from multiple threads simultaneously (it's non-atomic), so the compiler forbids sending an Rc across a thread boundary. Arc ("atomically reference counted") uses an atomic counter that is safe for concurrent updates, so it can be shared among threads. Arc is the thread-safe version of Rc; you pay a small extra cost for the atomic operations, which is why Rc still exists for single-threaded code.

You can now share data across threads safely, but what makes a type safe to share at all? The compiler decides this automatically using two marker traits. The next lesson (22.4) introduces Send and Sync, the quiet machinery that makes "fearless" a compile-time guarantee rather than a hope.