Expert · Concurrency and async · topic 37 of 50 · 35 min
Shared state: Arc, Mutex, Send and Sync
Explanation
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.
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()); // 7Arc: 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.
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
}thread 1 --Arc--+
thread 2 --Arc--+--> [ count | Mutex { locked?, data: 0 } ] (heap)
... |
thread 8 --Arc--+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.
| Tool | Use 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 |
| Channels | Handing 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
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.
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
}Practice (0/3 done)
Why does moving an Rc<Mutex<i32>> into thread::spawn fail to compile?
When is a std::sync::Mutex unlocked?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Async Rust with tokioConcurrency and async · same module
- Interview questions: traits, errors, pointers and concurrencyRust careers and interviews
- Concurrency 1: threads, mutexes and data racesC++ Programming (modern C++ and DSA) · Exceptions, concurrency and building projects with CMake
- Mutex, Once, atomic and the race detectorGo Programming (backend and cloud) · Concurrency: goroutines, channels and context
- Race conditions and synchronizedJava Programming (Core to Spring Boot) · Multithreading and concurrency
- Concurrency 2: atomics, async, futures and condition variablesC++ Programming (modern C++ and DSA) · Exceptions, concurrency and building projects with CMake
Questions people ask about this
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.
Which type should you use for shared ownership across threads?Related
Is the zero value of sync.Mutex ready to use?Related
Answer: Yes
var mu sync.Mutex is an unlocked, usable mutex.
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 more topic in “Concurrency and async” before its exam: Async Rust with tokio.
- Module 9 exam: Concurrency and async · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Testing, tooling and unsafe Rust. 50 topics left in the course before the final exam.