Shared State: Arc and Mutex

Moving data into a single thread is safe but limiting — sometimes multiple threads genuinely need to share and mutate the same data. This is exactly where data races live in other languages, and where Rust's guarantees shine brightest. The answer is a pair of types, `Arc` and `Mutex`, that let you share mutable state across threads — and the compiler will refuse to compile code that shares it unsafely. You literally cannot forget the lock, because the data lives inside it.

The previous post moved data into one thread. This post covers sharing mutable data across threads safely, using Arc (atomic reference counting for shared ownership) and Mutex (mutual exclusion for safe mutation). Together, Arc<Mutex<T>> is the standard Rust idiom for shared mutable state across threads — and the type system ensures you use it correctly. This is where Rust’s fearless concurrency handles the hardest case: genuinely shared state.

The problem: sharing across threads

To share data across threads, you need shared ownership (multiple threads owning the data) and safe mutation (no data races when mutating). Rust’s ownership rules make the naive approaches fail to compile — for good reason:

Sharing mutable state across threads needs thread-safe shared ownership and synchronized mutation — and Rust’s compiler forbids the unsafe approaches (Rc across threads, plain shared &mut), forcing you toward the safe tools. Those tools are Arc and Mutex.

Mutex: synchronized mutation

A Mutex<T> (mutual exclusion) protects data so only one thread can access it at a time — and in Rust, the data lives inside the mutex, so you can’t access it without locking:

use std::sync::Mutex;

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

    {
        // lock() returns a guard; access the data through it.
        let mut num = m.lock().unwrap();
        *num = 6;
    } // the lock is released here, when `num` goes out of scope

    println!("m = {m:?}");
}

A Mutex<T> provides synchronized mutation — the data lives inside it, lock() gives access via a guard that auto-releases (so you can’t forget to lock or unlock), and only one thread holds the lock at a time. But a single Mutex alone can’t be shared across threads (ownership) — that needs Arc.

Arc: thread-safe shared ownership

Arc<T> (Atomically Reference Counted) is the thread-safe version of Rc<T> — it provides shared ownership across threads:

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); // clone the Arc: another owner
        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()); // 10
}

Arc<Mutex<T>> combines Arc (thread-safe shared ownership) with Mutex (synchronized mutation) to safely share mutable state across threads — the standard Rust idiom. Each thread clones the Arc (shared owner) and locks the Mutex to mutate. The compiler ensures this is race-free. And it enforces exactly this correctness.

The compiler enforces correctness

The remarkable thing is that the compiler ensures you use shared state safely — you can’t easily get it wrong, which is fearless concurrency in action:

Shared mutable state across threads — concurrency’s hardest, most dangerous case — is made safe in Rust by Arc<Mutex<T>> (Arc for thread-safe shared ownership, Mutex for synchronized mutation with the data inside the lock), and the compiler enforces correct use (can’t share unsafely, can’t forget to lock). This is fearless concurrency for shared state. The traits that make it work — Send and Sync — are next.

Key takeaways

Further reading

Sources & References