How to share data between threads with Arc Mutex

Share data between Rust threads safely by wrapping it in Arc<Mutex<T>> and cloning the Arc for each thread.

When one owner isn't enough across threads

You are building a server. Three worker threads are processing requests. Each one needs to append a message to a shared log buffer. In Python, you pass the list and maybe slap a lock on it if you remember. In Rust, you try to pass the vector to the first thread, and the compiler rejects you. You try to clone the vector, and now you have three separate logs, which defeats the purpose. You need one log, shared safely, across threads that might die or live forever.

Rust's ownership rule says every value has exactly one owner. That rule keeps the language safe. It also blocks you when you need multiple threads to touch the same data. The solution is a combination of two types: Arc and Mutex. Arc handles shared ownership across threads. Mutex handles safe access to the data. You wrap your data in both, clone the Arc for each thread, and lock the Mutex whenever you touch the data.

The blueprint and the key

Arc stands for Atomic Reference Counted. It puts your data on the heap and tracks how many Arc instances point to it. When you clone an Arc, you don't copy the data. You increment a counter. When an Arc is dropped, the counter decrements. When the counter hits zero, the data is freed. The counter uses atomic operations, so it works safely across threads without data races.

Mutex stands for Mutual Exclusion. It ensures only one thread can access the data at a time. When a thread calls lock(), it waits until the lock is available. Once it gets the lock, it can read or write the data. When the lock guard is dropped, the lock is released.

Think of a safe in a bank. Arc is the list of people who have the combination. As long as one person has the combination, the safe exists. Mutex is the physical key. Only one person can hold the key at a time. You need the combination to find the safe, and the key to open it. Arc gives you the combination. Mutex gives you the key.

Arc<T> requires T to implement Send and Sync. Send means the type can be moved across threads. Sync means the type can be shared via reference across threads. Mutex<T> makes T Sync even if T isn't, because the mutex serializes access. This is how you share non-thread-safe data safely. The mutex wraps the data and enforces exclusive access, satisfying the compiler's requirements.

Minimal example

Here is the pattern. Create the data, wrap it in Mutex, wrap that in Arc, clone the Arc for each thread, and lock the Mutex inside the thread.

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

fn main() {
    // Create the shared data: a vector wrapped in Mutex, then wrapped in Arc.
    // Arc allows multiple owners across threads.
    // Mutex ensures only one thread modifies the vector at a time.
    let shared_data = Arc::new(Mutex::new(vec![1, 2, 3]));

    let mut handles = vec![];

    for _ in 0..3 {
        // Clone the Arc, not the data.
        // This increments the reference count, giving the new thread ownership.
        // Convention: use Arc::clone(&data) to make it clear you are cloning the Arc,
        // not doing a deep clone of the vector.
        let data_clone = Arc::clone(&shared_data);

        let handle = thread::spawn(move || {
            // Lock the mutex to get access.
            // lock() returns a Result. unwrap() panics if the mutex is poisoned.
            // A mutex is poisoned if a thread panics while holding the lock.
            // In production code, handle the PoisonError to recover the data.
            let mut guard = data_clone.lock().unwrap();
            guard.push(4);
            // guard is dropped here, releasing the lock.
        });
        handles.push(handle);
    }

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

    // Lock one last time to read the result.
    let final_data = shared_data.lock().unwrap();
    println!("{:?}", *final_data);
}

Clone the Arc, not the data. The convention Arc::clone(&data) signals to readers that you are sharing ownership, not copying the payload.

What happens under the hood

When you call Arc::new(Mutex::new(vec![1, 2, 3])), the vector is allocated on the heap. The Mutex wraps it. The Arc wraps the Mutex. The reference count starts at one.

Inside the loop, Arc::clone(&shared_data) creates a new Arc pointing to the same heap allocation. The reference count increments to two, then three, then four. Each clone is a lightweight pointer plus a counter bump. No data is copied.

thread::spawn takes a closure. The move keyword forces the closure to take ownership of data_clone. Without move, the closure would try to capture a reference, and the compiler would reject it because the thread might outlive the stack frame. move transfers the Arc into the thread's stack.

Inside the thread, data_clone.lock() tries to acquire the lock. If another thread holds the lock, this thread blocks. The OS puts the thread to sleep. When the other thread drops its MutexGuard, the lock is released. The sleeping thread wakes up, acquires the lock, and returns a MutexGuard.

The MutexGuard holds the lock. As long as the guard exists, no other thread can lock the mutex. When you call guard.push(4), you are mutating the vector through the guard. The guard ensures exclusive access. When the guard goes out of scope, its Drop implementation releases the lock.

When all threads finish, the Arc clones are dropped. The reference count decrements. When the last Arc is dropped in main, the count hits zero. The Mutex is dropped, which drops the vector, which frees the memory.

Realistic example

In real code, you often have a struct with methods. You wrap the struct in Arc<Mutex<T>> and call methods through the lock. Scope the lock carefully. Holding a lock longer than necessary blocks other threads and hurts performance.

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

/// A counter that tracks processed items.
/// Methods take &mut self, so they require exclusive access.
struct Counter {
    count: i64,
}

impl Counter {
    fn increment(&mut self) {
        self.count += 1;
    }

    fn get(&self) -> i64 {
        self.count
    }
}

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

    // Spawn 10 threads, each incrementing 100 times.
    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            for _ in 0..100 {
                // Scope the lock.
                // The guard is dropped at the end of the block, releasing the lock.
                // This allows other threads to acquire the lock sooner.
                {
                    let mut c = counter_clone.lock().unwrap();
                    c.increment();
                }
            }
        });
        handles.push(handle);
    }

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

    let final_count = counter.lock().unwrap().get();
    println!("Final count: {}", final_count);
}

Scope the lock. Release it the moment you are done. Holding a lock is expensive; holding it unnecessarily is a bug.

Pitfalls and compiler errors

Deadlocks are the biggest risk. A deadlock happens when two threads each hold a lock and wait for the other's lock. The program hangs. No error message appears. The threads sleep forever. Avoid deadlocks by establishing a global lock order. Always acquire locks in the same order across all threads. If you need multiple locks, lock them in alphabetical order of their names, or some other consistent rule.

Poisoned locks are another issue. If a thread panics while holding a lock, the mutex is marked as poisoned. Subsequent calls to lock() return an error. unwrap() panics on the error, which can cascade. Use into_inner() on the PoisonError to recover the guard and continue. This is safe because the panic might have left the data in a consistent state.

// Handling a poisoned lock
let guard = match counter_clone.lock() {
    Ok(g) => g,
    Err(poisoned) => poisoned.into_inner(),
};

If you try to share data that isn't Send, the compiler rejects you with E0277 (trait bound not satisfied). For example, Rc<T> is not Send. If you wrap Rc in a thread, you get an error. Use Arc instead. Arc is Send and Sync.

If you forget to lock the mutex before accessing the data, the compiler stops you. You can't dereference a Mutex directly. You must call lock() first. This prevents accidental data races at compile time.

Deadlocks don't crash. They freeze. If your program stops responding, check your lock order.

Decision matrix

Use Arc<Mutex<T>> when you need shared ownership and exclusive mutation across threads. Use Arc<RwLock<T>> when you have many readers and few writers, and the data is read-heavy. Use channels when threads need to send messages rather than share mutable state. Use AtomicUsize or AtomicBool when you need lock-free updates for simple values like counters or flags. Use Rc<Mutex<T>> when you need shared ownership and mutation but only within a single thread.

Shared state is hard. Channels are often easier. Pick the tool that matches your data flow.

Where to go next