Smart Pointers and Thread Safety
Thread safety for smart pointers is governed at compile time by two auto-traits in std::marker: Send (a value can be transferred across thread boundaries) and Sync (a shared reference &T can be accessed concurrently from multiple threads; equivalently, T is Sync if and only if &T is Send).
Send and Sync Matrix for Smart Pointers and Containers
| Type | Send Implementation Rule |
Sync Implementation Rule |
Reason |
|---|---|---|---|
Box<T> |
Send if T: Send |
Sync if T: Sync |
Unique owner; behaves identically to owning T directly |
Rc<T>, rc::Weak<T> |
Never Send |
Never Sync |
Uses non-atomic Cell<usize> reference counters |
Arc<T>, sync::Weak<T> |
Send if T: Send + Sync |
Sync if T: Send + Sync |
Shared &T across threads requires T: Sync; final drop on any thread requires T: Send |
Cell<T> |
Send if T: Send |
Never Sync |
Moving a Cell<T> to another thread is safe, but concurrent .set() via &Cell<T> would race |
RefCell<T> |
Send if T: Send |
Never Sync |
Runtime borrow counter is non-atomic |
Mutex<T> |
Send if T: Send |
Sync if T: Send |
Serializes all access; even non-Sync T (like RefCell) is accessed by one thread at a time |
RwLock<T> |
Send if T: Send |
Sync if T: Send + Sync |
Multiple threads can read &T simultaneously, requiring T: Sync |
MutexGuard<'_, T> |
Never Send |
Sync if T: Sync |
OS mutexes (such as pthread_mutex_t) must be unlocked by the same thread that locked them |
*const T, *mut T |
Never Send |
Never Sync |
Raw pointers carry no aliasing or synchronization guarantees |
Compile-Time Prevention of Thread-Unsafe Sharing (E0277)
Attempting to send an Rc<T> or Arc<RefCell<T>> into std::thread::spawn produces a compile-time error (E0277) before the program can ever execute a data race:
// ❌ does not compile
use std::cell::RefCell;
use std::sync::Arc;
use std::thread;
fn main() {
let shared_cell = Arc::new(RefCell::new(42));
thread::spawn(move || {
*shared_cell.borrow_mut() += 1;
});
}
error[E0277]: `RefCell<i32>` cannot be shared between threads safely
--> src/main.rs:8:19
|
8 | thread::spawn(move || {
| ------------- ^------
| | |
| | `RefCell<i32>` cannot be shared between threads safely
| required by a bound introduced by this call
|
= help: the trait `Sync` is not implemented for `RefCell<i32>`
= note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicI32` instead
= note: required for `Arc<RefCell<i32>>` to implement `Send`
Why Arc<T> Requires Both T: Send and T: Sync to Be Send
Moving an Arc<T> clone to another thread (Arc<T>: Send) has two consequences:
- Both the original thread and the new thread now hold
&Tsimultaneously $\implies$ requiresT: Sync. - Whichever thread drops the last
Arc<T>will runT::dropand deallocateTon that thread $\implies$ requiresT: Send.
Transferring a Non-Sync Type Safely with Arc<Mutex<T>>
Because Mutex<T> only requires T: Send to be both Send and Sync, wrapping a Send + !Sync type (such as Cell<T>, RefCell<T>, or an MPSC Sender<T> in older Rust versions) inside Mutex<T> allows Arc<Mutex<T>> to be shared across threads:
use std::cell::Cell;
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// `Cell<u32>` is `Send` but `!Sync`.
// Wrapping it in `Mutex` makes `Mutex<Cell<u32>>` both `Send` and `Sync`:
let shared = Arc::new(Mutex::new(Cell::new(10_u32)));
thread::scope(|s| {
let clone = Arc::clone(&shared);
s.spawn(move || {
let guard = clone.lock().unwrap();
guard.set(guard.get() + 25);
});
});
println!("final value: {}", shared.lock().unwrap().get());
}
final value: 35