Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 32: Shared State, Arc, Mutex, and Send/Sync

You will understand

  • Send vs Sync — the thread-safety gates
  • Arc<Mutex<T>> pattern and its tradeoffs
  • Why Rc/RefCell cannot cross thread boundaries

Reading time

45 min
+ 25 min exercises
Thread Traits

`Send` vs `Sync`

`Send` `Sync` T value may move to another thread ownership crosses the boundary &T shared reference may be used from multiple threads safely
Shared State

Arc<Mutex<T>> Separates Ownership from Access

Mutex inner T Arc Arc Arc MutexGuard `Arc` = many owners. `Mutex` = one mutable accessor at a time.

Step 1 - The Problem

Message passing is not enough for every design. Sometimes many threads need access to the same state:

  • a cache
  • a metrics registry
  • a connection pool
  • shared configuration or shutdown state

The classic failure mode is shared mutable access without synchronization. In C or C++, two threads incrementing the same counter through plain pointers create a data race. That is undefined behavior, not merely “a wrong answer sometimes.”

Even when you add locks manually, another problem remains: how do you encode, in types, which values are safe to move across threads and which are safe to share by reference across threads?

Step 2 - Rust’s Design Decision

Rust splits the problem in two.

  1. Ownership and borrowing still determine who can access a value.
  2. Auto traits determine whether a type may cross or be shared across thread boundaries.

Those auto traits are Send and Sync.

  • Send: ownership of this type may move to another thread
  • Sync: a shared reference to this type may be used from another thread

For shared mutable state, Rust does not permit “many aliases, everyone mutate if careful.” It requires a synchronization primitive whose API itself enforces access discipline. That is why Mutex<T> gives you a guard, not a raw pointer.

Step 3 - The Mental Model

Plain English rule: if multiple threads need the same data, separate the question of ownership from the question of access.

  • Arc<T> answers ownership: many owners
  • Mutex<T> answers access: one mutable accessor at a time
  • RwLock<T> answers access differently: many readers or one writer

And underneath all of it:

  • Send decides whether a value may move to another thread
  • Sync decides whether &T may be shared across threads

Step 4 - Minimal Code Example

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();

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

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

    assert_eq!(*counter.lock().unwrap(), 4);
}

Step 5 - Line-by-Line Compiler Walkthrough

  1. Arc::new(...) creates shared ownership with atomic reference counting.
  2. Mutex::new(0) wraps the integer in a synchronization primitive.
  3. Arc::clone(&counter) increments the atomic refcount; it does not clone the protected i32.
  4. thread::spawn(move || { ... }) moves one Arc<Mutex<i32>> handle into each thread.
  5. counter.lock() acquires the mutex and returns MutexGuard<i32>.
  6. Dereferencing the guard gives mutable access to the inner i32.
  7. When the guard goes out of scope, Drop unlocks the mutex automatically.

The invariant being checked is subtle but strong:

  • many threads may own handles to the same shared object
  • only the lock guard grants mutable access
  • unlocking is tied to scope exit through RAII

If you tried the same shape with Rc<RefCell<i32>>, thread::spawn would reject it because Rc<T> is not Send, and RefCell<T> is not Sync. That is not a missing convenience. It is the type system telling you those primitives were built for single-threaded aliasing, not cross-thread sharing.

Step 6 - Three-Level Explanation

Arc lets many threads own the same value. Mutex makes sure only one thread changes it at a time. The lock guard is like a temporary permission slip.

The common pattern is Arc<Mutex<T>> or Arc<RwLock<T>>, but mature Rust code treats that as a tool, not a default.

Use it when state is truly shared and long-lived. Do not use it as a reflex to silence the borrow checker. Many designs become simpler if you isolate ownership and send messages to a single state-owning task instead.

Send and Sync are unsafe auto traits. The compiler derives them structurally for safe code, but incorrect manual implementations can create undefined behavior. Rc<T> is !Send because non-atomic refcount updates would race. Cell<T> and RefCell<T> are !Sync because shared references to them do not provide thread-safe mutation discipline.

Arc<Mutex<T>> works because the components line up:

  • Arc provides thread-safe shared ownership
  • Mutex provides exclusive interior access
  • T is then accessed under a synchronization contract rather than raw aliasing

Send and Sync Precisely

TraitPrecise meaningTypical implication
SendA value of this type can be moved to another thread safelythread::spawn and tokio::spawn often require it
Sync&T can be shared between threads safelyMany shared references across threads require it

A useful equivalence to remember:

T is Sync if and only if &T is Send.

That sentence is dense, but it reveals Rust’s model: thread sharing is analyzed in terms of what references may do.

RwLock and Atomics

RwLock<T> is a better fit when reads are common, writes are rare, and the read critical sections are meaningful.

#![allow(unused)]
fn main() {
use std::sync::{Arc, RwLock};

let state = Arc::new(RwLock::new(String::from("ready")));
let read_guard = state.read().unwrap();
assert_eq!(&*read_guard, "ready");
}

Atomics are a better fit when the shared state is a small primitive with simple lock-free updates and carefully chosen memory ordering.

#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicUsize, Ordering};

let counter = AtomicUsize::new(0);
counter.fetch_add(1, Ordering::Relaxed);
}

Do not read this as “atomics are faster, so prefer them.” The right mental model is:

  • Mutex<T> for compound state and easy invariants
  • atomics for narrow state transitions you can reason about precisely

Deadlock and Lock Design

Rust prevents data races. It does not prevent deadlocks.

That distinction matters. A program can be memory-safe and still stall forever because two threads wait on each other.

The practical rules are old but still essential:

  • keep lock scopes short
  • avoid holding one lock while acquiring another
  • define a lock acquisition order if multiple locks are necessary
  • prefer moving work outside the critical section

Design Insight Rust eliminates unsynchronized mutation bugs, not bad concurrency architecture. You still need engineering judgment.

Step 7 - Common Misconceptions

Wrong model 1: “Arc makes mutation thread-safe.”

Why it forms: Arc is the cross-thread version of Rc, so people assume it solves all cross-thread problems.

Correction: Arc only solves shared ownership. It does nothing by itself about safe mutation.

Wrong model 2: “Mutex is a Rust replacement for borrowing.”

Why it forms: beginners often add a mutex when the borrow checker blocks them.

Correction: a mutex is a synchronization design choice, not a borrow-checker escape hatch.

Wrong model 3: “If it compiles, deadlock cannot happen.”

Why it forms: Rust’s safety guarantees feel broad.

Correction: Rust prevents data races, not logical waiting cycles.

Wrong model 4: “RwLock is always better for read-heavy workloads.”

Why it forms: more readers sounds automatically better.

Correction: RwLock has overhead, writer starvation tradeoffs, and can perform worse under real contention patterns.

Step 8 - Real-World Pattern

You will see Arc<AppState> in web services, often with inner members like pools, caches, or configuration handles. The best versions of those designs avoid wrapping the entire application state in one giant Mutex. Instead, they use:

  • immutable shared state where possible
  • fine-grained synchronization where necessary
  • owned messages to serialize stateful work

That pattern appears across async web services, observability pipelines, and long-running daemons. Mature code keeps the synchronized portion small and explicit.

Step 9 - Practice Block

Code Exercise

Build a small in-memory metrics registry with:

  • Arc<RwLock<HashMap<String, u64>>>
  • a writer thread that increments counters
  • two reader threads that snapshot the map periodically

Then explain whether a channel-based design would be simpler.

Code Reading Drill

What is being cloned here, and what is not?

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};

let state = Arc::new(Mutex::new(vec![1, 2, 3]));
let state2 = Arc::clone(&state);
}

Spot the Bug

What would go wrong conceptually if this compiled?

#![allow(unused)]
fn main() {
use std::cell::RefCell;
use std::rc::Rc;
use std::thread;

let data = Rc::new(RefCell::new(0));
thread::spawn(move || {
    *data.borrow_mut() += 1;
});
}

Refactoring Drill

Take a design that uses one Arc<Mutex<AppState>> containing twenty unrelated fields. Split it into a cleaner design and justify the new boundaries.

Compiler Error Interpretation

If the compiler says Rc<...> cannot be sent between threads safely, translate that as: “this type’s internal mutation discipline is not thread-safe, so the thread boundary is closed to it.”

Step 10 - Contribution Connection

After this chapter, you can read and modify:

  • shared service state initialization
  • lock-guarded caches
  • metrics counters and registries
  • thread-safe wrappers around non-thread-safe internals

Beginner-safe PRs include:

  • shrinking oversized lock scopes
  • replacing Arc<Mutex<T>> with immutable sharing where mutation is not needed
  • documenting Send and Sync expectations on public types

In Plain English

Sometimes many workers need access to the same thing. Rust separates “who owns it” from “who may touch it right now.” That matters to systems engineers because shared state is where performance, correctness, and operational bugs collide.

What Invariant Is Rust Protecting Here?

Shared access across threads must never create unsynchronized mutation or unsound aliasing. If a type crosses a thread boundary, its internal behavior must make that safe.

If You Remember Only 3 Things

  • Arc solves shared ownership, not shared mutation.
  • Send and Sync are the thread-safety gates the compiler uses to police concurrency boundaries.
  • Arc<Mutex<T>> is useful, but a design built entirely from it is often signaling missing ownership structure.

Memory Hook

Arc is the shared building deed. Mutex is the single key to the control room. Owning the building does not mean everyone gets to turn knobs at once.

Flashcard Deck

QuestionAnswer
What does Send mean?A value of the type can be moved to another thread safely.
What does Sync mean?A shared reference &T can be used from another thread safely.
Why is Rc<T> not Send?Its reference count is updated non-atomically, so cross-thread cloning or dropping would race.
Why is RefCell<T> not Sync?Its runtime borrow checks are not thread-safe synchronization.
What does Arc::clone clone?The pointer and atomic refcount participation, not the underlying protected value.
What unlocks a Mutex in idiomatic Rust?Dropping the MutexGuard, usually at scope end.
Does Rust prevent deadlock?No. Rust prevents data races, not waiting cycles.
When should you consider atomics instead of a mutex?When the shared state is a narrow primitive transition you can reason about with memory ordering semantics.

Chapter Cheat Sheet

SituationPreferred toolReason
Shared ownership, no mutationArc<T>Cheap clone of ownership handle
Shared mutable compound stateArc<Mutex<T>>Exclusive access with simple invariants
Read-heavy shared stateArc<RwLock<T>>Many readers, one writer
Single integer or flag with simple updatesatomicsNo lock, explicit memory ordering
Single-threaded shared ownershipRc<T>Cheaper than Arc, but not thread-safe