Skip to content

Expert · Concurrency and async · topic 37 of 50 · 35 min

Shared state: Arc, Mutex, Send and Sync

Explanation

To share changeable data between threads, wrap it in a Mutex (only one thread can lock it at a time) and share the Mutex with Arc (an atomically reference-counted pointer). lock() returns a guard that unlocks automatically when dropped. The marker traits Send and Sync tell the compiler which types may move to or be shared between threads; Rc is neither, so it cannot cross threads.

When messages are not enough

Sometimes several threads must update the same thing: a counter of website visits, a cache of exchange rates, a shared inventory. For this you need shared state with synchronisation, meaning a rule that only one thread changes the data at a time.

Mutex: one thread at a time

A Mutex (mutual exclusion) protects data so only one thread can access it at a time. To use the data, a thread calls lock(). If another thread holds the lock, the caller waits. lock() returns a MutexGuard, a smart pointer to the data. When the guard is dropped, the lock is released automatically, so you cannot forget to unlock.

rust
use std::sync::Mutex;

let stock = Mutex::new(10);
{
    let mut s = stock.lock().unwrap();   // lock; s is a MutexGuard<i32>
    *s -= 3;
}                                        // guard dropped: unlocked here
println!("{:?}", stock.lock().unwrap()); // 7
lock() returns a Result: Err if another thread panicked while holding the lock (the mutex is "poisoned").
Think of it like this: A Mutex is like the single key to the office stationery cupboard. Whoever has the key can open the cupboard; everyone else waits. The key is returned automatically when you walk away from the cupboard (the guard is dropped), so nobody can keep it by mistake.

Arc: sharing ownership across threads

Each thread needs to own a handle to the Mutex. Rc cannot be used, because its counter is not thread-safe. Arc<T> (atomically reference counted) is the same idea as Rc, but it updates the count with atomic CPU instructions that are safe across threads. They cost slightly more, which is why Rc also exists.

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

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

    for _ in 0..8 {
        let visits = Arc::clone(&visits);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                *visits.lock().unwrap() += 1;
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("{}", *visits.lock().unwrap());   // always 8000
}
Without the Mutex, two threads could read the same old value and both write +1, losing counts.
output
thread 1 --Arc--+
thread 2 --Arc--+--> [ count | Mutex { locked?, data: 0 } ]   (heap)
   ...           |
thread 8 --Arc--+
Arc shares ownership; Mutex controls access.

Send and Sync in plain words

  • Send: a value of this type can be moved to another thread. Most types are Send. Rc is not, because two threads could update its counter at once.
  • Sync: a &T (a shared reference) can be used from several threads at once. Mutex<T> is Sync (when T is Send), which is why Arc<Mutex<T>> works. RefCell is not Sync.

You almost never implement these by hand; the compiler works them out from a type's fields. Their job is to make thread mistakes compile errors: try to move an Rc into thread::spawn and you get "Rc<...> cannot be sent between threads safely".

Deadlocks and other choices

A deadlock happens when thread A holds lock 1 and waits for lock 2, while thread B holds lock 2 and waits for lock 1. Both wait forever. Rust does not prevent deadlocks. Avoid them by keeping locks short, never calling unknown code while holding a lock, and always taking several locks in the same order.

ToolUse when
Mutex<T>Any shared data that threads change
RwLock<T>Many readers, few writers: many read() at once, write() is exclusive
AtomicUsize, AtomicBool...A single number or flag; fastest, no lock
ChannelsHanding work or results from thread to thread

Common mistakes

  • Using Rc instead of Arc with threads: compile error "cannot be sent between threads safely".
  • Holding a lock guard for a long time (for example across a slow file write). Copy the data out, drop the guard, then do slow work.
  • Locking the same Mutex twice in one thread: the second lock waits forever (or panics on some platforms). std's Mutex is not re-entrant.
  • Forgetting Arc::clone before move, so the first thread takes the only Arc.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Shared state: Arc, Mutex, Send and Sync · 1/7

Shared counter, cache, stock

When several threads must change the same data, you need shared state with a lock.

Prefer a video by a teacher? Find free videos on “rust arc mutex shared state threads send sync” on YouTube ↗

In real life: Live seat count for a concert

A ticketing service sells seats for a popular concert from many server threads at once. The remaining seat count is kept in an Arc<Mutex<u32>>. Each sale locks the count, checks that a seat is left, decreases it, and unlocks. Two buyers can never get the last seat, which is exactly the bug that happens when a counter is shared without a lock.

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

fn main() {
    let seats = Arc::new(Mutex::new(5u32));
    let sold = Arc::new(Mutex::new(0u32));
    let handles: Vec<_> = (0..20).map(|_| {
        let seats = Arc::clone(&seats);
        let sold = Arc::clone(&sold);
        thread::spawn(move || {
            let mut left = seats.lock().unwrap();
            if *left > 0 {
                *left -= 1;
                *sold.lock().unwrap() += 1;
            }
        })
    }).collect();
    for h in handles { h.join().unwrap(); }
    println!("sold {}, left {}", sold.lock().unwrap(), seats.lock().unwrap());   // sold 5, left 0
}
20 buyers, 5 seats: exactly 5 are sold, every time.

Practice (0/3 done)

Task 1 · Question

Why does moving an Rc<Mutex<i32>> into thread::spawn fail to compile?

Task 2 · Question

When is a std::sync::Mutex unlocked?

Task 3 · Practice

Build a word-frequency counter where 4 threads each process a quarter of a list of sentences and update one shared Arc<Mutex<HashMap<String, u32>>>. Print the 3 most common words. Then rewrite it so each thread builds its own HashMap and returns it, and the main thread merges them. Note which version locks less.

Quick quiz

Question 1

1.What does Arc stand for?

Question 2

2.What does a Mutex guarantee?

Question 3

3.Which statement about Rust and deadlocks is true?

Question 4

4.Many threads read a config and rarely update it. Which is a good fit?

Question 5

5.What does "poisoned" mean for a Mutex?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does Arc stand for?

Answer: Atomically reference counted

Arc uses atomic operations for its count.

What does a Mutex guarantee?

Answer: Only one thread accesses the data at a time

Mutual exclusion is its purpose.

Which statement about Rust and deadlocks is true?

Answer: Deadlocks are still possible; avoid them by design

Rust prevents data races, not deadlocks.

Many threads read a config and rarely update it. Which is a good fit?

Answer: RwLock

RwLock allows many readers at once.

What does "poisoned" mean for a Mutex?

Answer: A thread panicked while holding the lock

lock() then returns Err so you can decide what to do.

Is RefCell<T> safe to share between threads?Related

Answer: No, it is not Sync

Use Mutex or RwLock across threads.

From Interview questions: traits, errors, pointers and concurrency

Why use std::lock_guard instead of calling m.lock() and m.unlock() yourself?Related

Answer: It unlocks automatically, even when an exception is thrown (RAII)

Manual unlock is easy to miss on an early return or exception.

From Concurrency 1: threads, mutexes and data races

Which type should you use for shared ownership across threads?Related

Answer: Arc

Arc uses atomic counting, so it is thread-safe.

From Rc, RefCell and Weak

Is the zero value of sync.Mutex ready to use?Related

Answer: Yes

var mu sync.Mutex is an unlocked, usable mutex.

From Mutex, Once, atomic and the race detector

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Concurrency and async” before its exam: Async Rust with tokio.
  2. Module 9 exam: Concurrency and async · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Testing, tooling and unsafe Rust. 50 topics left in the course before the final exam.

← Previous: Message passing with channels