Send, Sync & Shared-State Concurrency
Reviewed & published by Brayan K
Rust's "fearless concurrency" rests on two marker traits — Send and Sync — that let the compiler reject data races before your program even runs.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll spawn threads with move closures, share mutable state safely with Arc and Mutex, see why Rc and RefCell stay single-threaded, and learn to dodge deadlocks.
What You'll Learn in This Lesson
- The Send and Sync auto traits
- What makes a type thread-safe
- thread::spawn and move closures
- Sharing ownership with Arc<T>
- Arc<Mutex<T>> and RwLock for mutation
- Why Rc/RefCell aren't Send/Sync; deadlocks
1️⃣ Spawning Threads with move
thread::spawn runs a closure on a new OS thread, and the data you capture must be Send. The move keyword transfers ownership of captures into the thread so they outlive the spawning scope.
// thread::spawn runs a closure on a new OS thread.
// 'move' transfers ownership of captured data into the thread.
use std::thread;
fn main() {
let data = vec![1, 2, 3];
// Without 'move', the closure would only borrow 'data', which can't
// safely outlive main's scope. 'move' gives the thread ownership.
let handle = thread::spawn(move || {
let sum: i32 = data.iter().sum();
println!("thread sum = {sum}");
sum
});
// join() waits for the thread and returns its result.
let result = handle.join().unwrap();
println!("joined = {result}");
}Because Vec<i32> is Send, the compiler allows moving it across the thread boundary, and join returns the thread's result.
2️⃣ Shared Mutable State with Arc<Mutex<T>>
To mutate one value from many threads, combine two tools: Arc gives atomically reference-counted shared ownership, and Mutex gives synchronized interior mutability. Locking returns a guard that releases automatically when dropped.
// Share MUTABLE state across threads with Arc<Mutex<T>>.
// Arc = atomically reference-counted shared OWNERSHIP.
// Mutex = synchronized interior mutability (one writer at a time).
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 {
// Clone the Arc: a new handle to the SAME counter (cheap, atomic).
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
// lock() returns a guard; the lock is released when it drops.
let mut n = counter.lock().unwrap();
*n += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("final = {}", *counter.lock().unwrap());
}For read-heavy data, swap Mutex for RwLock, which lets many readers proceed concurrently while still serializing writers.
3️⃣ Why Rc and RefCell Stay Single-Threaded
Send and Sync are auto traits: the compiler grants them only when every part of a type is safe. Rc uses a non-atomic count and RefCell non-atomic borrow flags, so neither qualifies — using one across threads is a compile error.
// Why Rc<RefCell<T>> can't cross threads: it isn't Send or Sync.
// Send and Sync are AUTO traits the compiler adds when it's safe.
use std::rc::Rc;
fn main() {
let shared = Rc::new(5);
// This does NOT compile. Rc's reference count is non-atomic, so the
// compiler refuses to send it to another thread.
//
// thread::spawn(move || println!("{}", shared));
// error: Rc<i32> cannot be sent between threads safely
//
// The fix is Arc, whose count IS atomic and therefore Send + Sync.
println!("Rc holds {}, reference count {}", shared, Rc::strong_count(&shared));
println!("Rc is single-threaded; use Arc to cross threads");
}The compiler catching this at build time is exactly what "fearless concurrency" means: the unsafe sharing never reaches runtime.
Mini-Challenge: A Shared Counter
Use Arc<Mutex<T>> to total a value across three threads.
// 🎯 MINI-CHALLENGE: sum with a shared counter.
//
// 1. Create let total = Arc::new(Mutex::new(0));
// 2. Spawn 3 threads. In each, Arc::clone the total, lock it, and add 10.
// 3. Join all threads, then print the locked total.
//
// Expected output: total = 30
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// your code here
}Common Errors (and the fix)
- "closure may outlive the current function" — Add move to take ownership of captures.
- "`Rc<T>` cannot be sent between threads" — Use Arc<T> instead of Rc<T>.
- "cannot borrow data as mutable" — Wrap shared state in a Mutex or RwLock.
- Program hangs forever — A deadlock; acquire locks in a consistent order and keep guards short-lived.
Pro Tips
- 💡 Clone the Arc, not the data: Arc::clone is a cheap atomic bump, not a deep copy.
- 💡 Drop the guard early: let the MutexGuard go out of scope before doing slow work.
- 💡 Prefer channels when you can: share memory by communicating to avoid locks entirely.
📋 Quick Reference — Concurrency
| Goal | Rust Syntax |
|---|---|
| Spawn a thread | thread::spawn(move || { }) |
| Wait for it | handle.join().unwrap() |
| Shared ownership | Arc::new(value) |
| Shared mutation | Arc<Mutex<T>> |
| Lock it | m.lock().unwrap() |
| Many readers | RwLock<T> |
🎉 Lesson Complete!
- ✅ Send and Sync are auto traits that gate thread safety at compile time
- ✅ thread::spawn with move sends owned, Send data to a new thread
- ✅ Arc<Mutex<T>> (or RwLock) shares mutable state safely
- ✅ Rc/RefCell are single-threaded; consistent lock ordering avoids deadlocks
Practice quiz
What does the Send marker trait indicate about a type?
- Ownership of it can be transferred to another thread
- It is immutable
- It is heap allocated
- It can be cloned
Answer: Ownership of it can be transferred to another thread. A type is Send if it is safe to move its ownership to a different thread.
What does the Sync marker trait indicate about a type T?
- T is copyable
- T uses no heap
- &T can be shared across threads safely
- T is serializable
Answer: &T can be shared across threads safely. T is Sync when &T is Send, meaning a shared reference can be accessed from multiple threads safely.
How are Send and Sync normally implemented?
- Manually for every type
- Automatically (auto traits) when all fields qualify
- By a derive macro you write
- By calling .sync()
Answer: Automatically (auto traits) when all fields qualify. Send and Sync are auto traits: the compiler implements them automatically when all of a type's parts already are.
Why is Rc<T> NOT Send or Sync?
- It lives on the stack
- It cannot be cloned
- It is too large
- Its reference count is non-atomic, so concurrent updates would race
Answer: Its reference count is non-atomic, so concurrent updates would race. Rc uses a non-atomic reference count, so sharing it across threads could corrupt the count; that's why it isn't Send or Sync.
Which smart pointer gives thread-safe shared ownership?
- Arc<T>
- Box<T>
- Rc<T>
- Cell<T>
Answer: Arc<T>. Arc<T> is the atomically reference-counted pointer, safe to clone and share across threads.
To share MUTABLE state across threads, you typically wrap it in...
- Cell<T>
- Arc<Mutex<T>>
- Rc<RefCell<T>>
- Box<T>
Answer: Arc<Mutex<T>>. Arc gives shared ownership across threads and Mutex provides synchronized interior mutability.
What does a thread::spawn closure usually need the move keyword for?
- To inline it
- To make it async
- To copy the stack
- To take ownership of captured variables into the new thread
Answer: To take ownership of captured variables into the new thread. move forces the closure to own its captures so they can safely outlive the spawning scope on the new thread.
When is RwLock often preferred over Mutex?
- When data is tiny
- When you need a non-atomic count
- When there are many readers and few writers
- When there is no contention ever
Answer: When there are many readers and few writers. RwLock allows many concurrent readers OR one writer, which helps read-heavy workloads.
What causes a classic deadlock with two mutexes?
- Reading without a lock
- Two threads each holding one lock and waiting for the other
- Using a channel
- Cloning an Arc
Answer: Two threads each holding one lock and waiting for the other. If threads acquire two locks in opposite orders, each can wait forever for the other; consistent lock ordering avoids it.
What is the idiomatic alternative to sharing mutable state directly?
- Message passing over channels
- Disabling the borrow checker
- Global mutable statics
- Spinning in a loop
Answer: Message passing over channels. Rust encourages 'do not communicate by sharing memory; share memory by communicating' using channels like mpsc.
Continue this course
- Previous: Error Handling with thiserror & anyhow
- Next: Procedural & Derive Macros — Write your own derive, attribute and function-like macros with syn and quote
- Quick reference: Rust cheat sheet
Frequently asked questions
What do Send and Sync actually mean?
They are marker traits — traits with no methods — that the compiler uses to track thread safety. A type is Send if it is safe to transfer its ownership to another thread; a type is Sync if it is safe for multiple threads to hold shared references (&T) to it at once, which is formally defined as &T being Send. Almost every ordinary type is both: integers, String, Vec, and structs built from such fields qualify automatically. These traits are how Rust enforces 'fearless concurrency' at compile time: thread::spawn requires the data you move in to be Send, so an unsafe sharing mistake becomes a compile error rather than a runtime data race.
Why are Send and Sync called auto traits?
They are 'auto traits', meaning the compiler implements them for your type automatically whenever every component of the type already implements them — you almost never write impl Send or impl Sync yourself. A struct whose fields are all Send is itself Send; a struct whose fields are all Sync is itself Sync. The trait is only absent when a type contains something that explicitly opts out, such as a raw pointer or a non-atomic reference count. Manually implementing them is possible but unsafe, because you are promising the compiler a guarantee it could not verify, which is something only low-level building blocks need to do.
Why can't Rc and RefCell be shared across threads, and what replaces them?
Rc<T> keeps a non-atomic reference count, so if two threads cloned or dropped it at the same moment the count could be corrupted, leading to use-after-free; therefore Rc is neither Send nor Sync. RefCell<T> tracks its borrow state with ordinary non-atomic flags and would race the same way, so it is not Sync. Their multithreaded counterparts are Arc<T> for shared ownership (its count uses atomic operations) and a lock — Mutex<T> or RwLock<T> — for interior mutability. The common pattern for shared mutable state across threads is therefore Arc<Mutex<T>> (or Arc<RwLock<T>> when reads dominate), while Rc<RefCell<T>> stays in single-threaded code.
How do I avoid deadlocks, and is there a simpler model?
A deadlock happens when threads wait on each other forever — classically when two threads each hold one lock and try to acquire the other in the opposite order. You prevent it by always acquiring multiple locks in a consistent global order, keeping critical sections short, and releasing a guard (by letting it drop) before doing slow work. RwLock helps read-heavy workloads by allowing many simultaneous readers or one writer. Often the cleaner answer is to avoid shared mutable state altogether: Rust's motto is 'do not communicate by sharing memory; share memory by communicating', so passing ownership of data through channels (mpsc) sidesteps both locks and deadlocks entirely.