RustArc<T> & Mutex<T>

Arc<T> and Mutex<T>

Safe concurrency in Rust rests on two guarantees: the ownership system prevents data races at compile time, and the standard library's Arc and Mutex types provide the controlled exceptions when multiple threads genuinely need to share data.

This page covers how Arc<T> shares ownership across threads and how Mutex<T> serialises access to mutable state — and the canonical pattern that combines both.

Arc<T>: Atomically Reference Counted

Arc<T> stands for Atomically Reference Counted. It is the thread-safe counterpart to Rc<T>: both let multiple owners share the same heap-allocated value, but Arc uses atomic CPU instructions to update the reference count, making it safe to use from several threads simultaneously.

Rc<T>

Arc<T>

Thread-safe

No

Yes

Reference counting

Plain integer

Atomic integer

Performance

Faster

Slightly slower

Use when

Single-threaded sharing

Multi-threaded sharing

The overhead of atomic operations is usually negligible in practice, but it is real — prefer Rc in single-threaded code and reach for Arc only when you need to cross a thread boundary.

Creating and cloning an Arc:

RUST
use std::sync::Arc;

fn main() {
    // Create a new Arc — value is heap-allocated, reference count = 1
    let shared = Arc::new(vec![1, 2, 3]);

    // Arc::clone increments the reference count atomically
    let also_shared = Arc::clone(&shared);

    println!("original: {:?}", shared);
    println!("clone:    {:?}", also_shared);
    println!("strong count: {}", Arc::strong_count(&shared)); // 2
}   // both handles drop here; count falls to 0 and the Vec is freed
original: [1, 2, 3]
clone:    [1, 2, 3]
strong count: 2
Note
Always write Arc::clone(&arc) rather than calling.clone() on the value directly. Both compile to the same thing, but the explicit form makes it clear you are incrementing a reference count — not deep-cloning the inner data.
Sharing an Arc Across Threads

Passing an Arc to a spawned thread requires moving a clone into the thread's move closure. Each thread gets its own handle, and the underlying data is freed only when the last handle is dropped.

RUST
use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![10, 20, 30]);
    let mut handles = vec![];

    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let sum: i32 = data_clone.iter().sum();
            println!("Thread {}: sum = {}", i, sum);
        });
        handles.push(handle);
    }

    for h in handles {
        h.join().unwrap();
    }
}
Thread 0: sum = 60
Thread 1: sum = 60
Thread 2: sum = 60
Tip
Arc<T> by itself only gives immutable shared access. If multiple threads need to mutate the shared value, pair it withMutex<T> — that is exactly what the next section covers.
Mutex<T>: Mutual Exclusion

Mutex<T> wraps a value and enforces that only one thread can access it at a time. Any thread that wants the data must first acquire the lock; all other threads block until the current holder releases it.

In Rust, the data is stored inside the Mutex. You cannot access the value without going through lock(), so a data race is structurally impossible.

RUST
use std::sync::Mutex;

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

    {
        // lock() blocks until we acquire the lock.
        // Returns MutexGuard<i32> — a smart pointer to the inner value.
        let mut val = m.lock().unwrap();
        *val += 1;
        println!("val inside lock: {}", *val);
        // MutexGuard is dropped here — lock is automatically released (RAII)
    }

    println!("val after lock released: {}", *m.lock().unwrap());
}
val inside lock: 6
val after lock released: 6
Note
MutexGuard implements Deref andDerefMut, so you can use it exactly like a mutable reference. When the guard goes out of scope, the lock is released automatically — this is Rust's RAII (Resource Acquisition Is Initialisation) pattern.
The Canonical Pattern: Arc<Mutex<T>>

Combining Arc and Mutex gives you shared, mutable state that is safe across threads. The Arc keeps the Mutex alive across all threads, and the Mutex serialises write access.

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

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

    for _ in 0..5 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
            // num (MutexGuard) dropped here — lock released
        });
        handles.push(handle);
    }

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

    println!("Final counter: {}", *counter.lock().unwrap());
}
Final counter: 5

Every increment is protected: only one thread holds the MutexGuard at a time, so the read-modify-write is atomic from the perspective of other threads. No data race, no undefined behaviour.

Deadlocks

A deadlock occurs when two or more threads each hold a lock that another needs, and both wait forever for the other to release it. Rust's type system prevents data races but cannot prevent deadlocks — that is a logic error you must avoid.

Warning
The most common deadlock is acquiring the same lock twice in one thread.std::sync::Mutex is not re-entrant: calling lock()while you already hold the lock will block the thread forever.

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

fn main() {
    let lock = Arc::new(Mutex::new(0));
    let _guard1 = lock.lock().unwrap(); // lock acquired
    // The line below will hang forever — this thread already holds the lock
    // let _guard2 = lock.lock().unwrap(); // DEADLOCK
}

// Safe approach — let the guard drop before re-locking
fn update_twice(m: &Mutex<i32>) {
    {
        let mut v = m.lock().unwrap();
        *v += 1;
    } // guard dropped, lock released

    {
        let mut v = m.lock().unwrap();
        *v += 1;
    } // guard dropped, lock released
}
  • Always acquire multiple locks in the same order across all threads

  • Keep the scope of a lock guard as small as possible — drop it early

  • Never call a function that may acquire the same lock while you're holding a guard

  • Use try_lock() during development to surface potential deadlock sites

Poisoned Mutexes

If a thread panics while holding a Mutex lock, the mutex is marked as poisoned. Subsequent calls to lock() return Err(PoisonError) to prevent other threads from silently operating on potentially inconsistent data.

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

fn main() {
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));
    let data_clone = Arc::clone(&data);

    // This thread panics while holding the lock — mutex becomes poisoned
    let _ = thread::spawn(move || {
        let mut v = data_clone.lock().unwrap();
        v.push(4);
        panic!("something went wrong!"); // lock poisoned
    })
    .join(); // returns Err because the thread panicked

    match data.lock() {
        Ok(v) => println!("got value: {:?}", *v),
        Err(poisoned) => {
            // We can recover the inner value if we know it is still valid
            let v = poisoned.into_inner();
            println!("recovered poisoned value: {:?}", *v);
        }
    }
}
recovered poisoned value: [1, 2, 3, 4]
Note
In many applications, calling .unwrap() on a poisoned lock is the right choice — if any thread panicked, you likely want to propagate the failure. Use the Err branch only when you know the partially modified state is still valid and safe to use.
RwLock<T>: Multiple Readers or One Writer

RwLock<T> is a variant of Mutex<T> optimised for read-heavy workloads. It allows either many concurrent readers or one exclusive writer — never both simultaneously. Use it when reads far outnumber writes and you want higher throughput.

RUST
use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let config = Arc::new(RwLock::new(String::from("initial")));
    let mut handles = vec![];

    // Four reader threads — they can all run concurrently
    for i in 0..4 {
        let cfg = Arc::clone(&config);
        handles.push(thread::spawn(move || {
            let val = cfg.read().unwrap(); // shared read lock
            println!("Reader {}: {}", i, *val);
        }));
    }

    // One writer — waits until all readers have released their locks
    {
        let cfg = Arc::clone(&config);
        handles.push(thread::spawn(move || {
            let mut val = cfg.write().unwrap(); // exclusive write lock
            *val = String::from("updated");
            println!("Writer: set to '{}'", *val);
        }));
    }

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

Mutex<T>

RwLock<T>

Concurrent reads

No — one thread at a time

Yes — any number of readers

Exclusive write

Yes

Yes

Best for

Write-heavy or balanced

Read-heavy workloads

Deadlock risk

Lower

Slightly higher (writer starvation possible)

Arc+Mutex vs Channels: When to Use Which

Rust gives you two primary tools for inter-thread communication:

  • Channels (std::sync::mpsc) — send messages, transfer ownership of data
  • Arc+Mutex — share state in-place, serialise access with a lock

Neither is universally better; they model different problems.

Scenario

Prefer

One thread produces data, another consumes it

Channel

A value is read and written by many threads

Arc+Mutex

You want to avoid shared mutable state entirely

Channel

Low-latency reads of cached, rarely-written data

Arc+RwLock

Fan-out work queue

Channel with multiple senders

Accumulating a result across threads

Arc+Mutex

Success
Rust makes it impossible to accidentally share mutable data across threads without a synchronisation primitive. The compiler rejects any attempt — so if your program compiles and runs, it is free of data races by construction.