Code of the Day
AdvancedConcurrency

Shared state

Share data safely across threads using Arc for reference counting and Mutex or RwLock for interior mutability — and understand Send and Sync.

RustAdvanced12 min read
Recommended first
By the end of this lesson you will be able to:
  • Use Arc<T> to share ownership of data across threads
  • Lock a Mutex<T> to gain exclusive mutable access
  • Use RwLock<T> for concurrent read access with exclusive write access
  • Explain the Send and Sync marker traits and what they guarantee
  • Identify deadlock patterns and how to avoid them

When threads need to read or modify the same data, ownership alone isn't enough — one value can't have multiple owners at once. Rust provides two composable tools: for shared ownership and or RwLock<T> for safe mutation. Combined, they form the idiomatic pattern for shared mutable state.

Arc<T>: shared ownership across threads

Arc is "Atomically Reference Counted." Like Rc<T> (the single-threaded version), it lets multiple owners share a value. Unlike Rc<T>, Arc uses atomic operations for its reference count — safe to use across threads.

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

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

    let handles: Vec<_> = (0..3).map(|_| {
        let data = Arc::clone(&data);  // clone the Arc, not the Vec
        thread::spawn(move || {
            println!("{:?}", data);
        })
    }).collect();

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

Arc::clone increments the reference count. When the last Arc pointing to the data is dropped, the data is freed. Each thread gets its own Arc handle — all pointing at the same Vec.

Arc alone only gives shared immutable access. If you need to mutate the shared data, wrap it in a Mutex or RwLock inside the Arc.

Mutex<T>: exclusive access

Mutex provides mutual exclusion: at most one thread can access the inner value at a time. You call lock() to acquire the lock; it returns a MutexGuard that derefs to &mut T. When the guard is dropped, the lock is released.

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

fn main() {
    let counter = Arc::new(Mutex::new(0u64));

    let handles: Vec<_> = (0..10).map(|_| {
        let counter = Arc::clone(&counter);
        thread::spawn(move || {
            let mut guard = counter.lock().unwrap();
            *guard += 1;
        })
    }).collect();

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

    println!("Final: {}", counter.lock().unwrap());  // 10
}

Arc<Mutex<T>> is the canonical pattern for shared mutable state in Rust.

Deadlock patterns

A occurs when two threads each hold a lock the other is waiting for. Rust's type system doesn't prevent deadlocks — they're a logic problem, not a type problem:

// Thread A holds lock1, waiting for lock2
// Thread B holds lock2, waiting for lock1
// — both wait forever

Common rules to avoid deadlocks:

  1. Consistent lock ordering: always acquire multiple locks in the same order everywhere.
  2. Minimal lock scope: hold locks for the shortest time possible. Don't call user code while holding a lock.
  3. Prefer channels when you can: pass data instead of sharing it.

Calling lock().unwrap() will panic if the mutex is poisoned — which happens when another thread panicked while holding the lock. In production code, handle PoisonError explicitly or use lock().unwrap_or_else(|e| e.into_inner()) to recover the guard despite the poison.

RwLock<T>: concurrent reads, exclusive writes

RwLock allows multiple concurrent readers or one exclusive writer, but never both. This is more efficient than a Mutex when reads are frequent and writes are rare:

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

fn main() {
    let config = Arc::new(RwLock::new(String::from("initial")));

    // Multiple reader threads
    let readers: Vec<_> = (0..3).map(|i| {
        let config = Arc::clone(&config);
        thread::spawn(move || {
            let value = config.read().unwrap();
            println!("reader {}: {}", i, *value);
        })
    }).collect();

    // One writer thread
    let config_w = Arc::clone(&config);
    let writer = thread::spawn(move || {
        let mut value = config_w.write().unwrap();
        *value = String::from("updated");
    });

    for r in readers { r.join().unwrap(); }
    writer.join().unwrap();
}

Send and Sync marker traits

Two marker traits underpin Rust's thread safety:

  • : a type can be transferred to another thread. Most types are Send. Rc<T> is not (its reference count isn't atomic).
  • : a type can be shared across threads (i.e., &T is Send). Mutex<T> is Sync (sharing a reference to it is safe); Cell<T> is not.

These are auto-implemented by the compiler based on the types of a struct's fields. You rarely implement them manually. When the compiler says "the trait Send is not implemented," it's telling you that something in your type can't safely cross thread boundaries.

// This won't compile:
let rc = std::rc::Rc::new(42);
thread::spawn(move || println!("{}", rc));
// error: Rc<i32> cannot be sent between threads safely
// Use Arc<i32> instead.

Check your understanding

Knowledge check

  1. 1.
    Why must you use Arc<T> instead of Rc<T> for shared ownership across threads?
  2. 2.
    How does Mutex<T> ensure exclusive access without requiring you to call an explicit unlock?
  3. 3.
    When is RwLock a better choice than Mutex?

Do it yourself

Build a thread-safe hit counter: spawn 20 threads, each incrementing a Arc<Mutex<u64>> 1000 times. Join all threads. The final count should be exactly 20,000. If you see a different number, you have a synchronisation bug — the type system won't let you have one here, but it's useful to verify.

Where to go next

Shared state with locks is one model for inter-thread communication. The other model — channels — passes data between threads rather than sharing it. Channels are often simpler and less error-prone. Next lesson covers std::sync::mpsc.

Finished reading? Mark it complete to track your progress.

On this page