Send + Sync (auto-traits)
"Fearless concurrency" is two empty traits and a derivation rule. Send and Sync carry no methods and generate no code — they are facts the compiler infers about every type and checks at every thread::spawn. This page covers how the inference works, why Rc fails it and Arc passes, the genuinely subtle bounds on Mutex and MutexGuard, and what you sign up for when you write unsafe impl Send.
Long read · the definitions through std's impls and the unsafe escape hatch · references at the end
1 · The two definitions
Both traits live in std::marker and are about which thread may touch a value:
T: Send— ownership of aTcan move to another thread. The destination thread may use and drop it.T: Sync— a&Tcan be shared with another thread. Formally:T: Syncif and only if&T: Send. That's the actual definition, not an analogy.
pub unsafe auto trait Send {}
pub unsafe auto trait Sync {}
// "unsafe": a wrong manual impl causes UB, not a logic bug.
// "auto": the compiler implements them for you, structurally.
// No methods. Pure compile-time facts.A data race needs two threads reaching the same memory with at least one write and no synchronisation. Send and Sync make the "two threads reaching the same memory" part impossible to express in safe code unless every type involved has declared it can handle it. The proof lives in the type system; at runtime these traits don't exist.
2 · Auto-trait derivation — structural, recursive, silent
You never derive Send or Sync. For every type definition the compiler applies one rule: a struct or enum is Send if all its fields are Send; Sync if all its fields are Sync. Recursively, down to the leaves. Primitives are both. So almost everything you write is both, automatically — the property is infectious in the good direction.
use std::rc::Rc;
struct Job { // Send + Sync: all fields are
id: u64,
payload: Vec<u8>,
}
struct Cached { // NOT Send, NOT Sync:
job: Job,
memo: Rc<String>, // <- one Rc poisons the whole struct
}
fn assert_send<T: Send>() {}
fn main() {
assert_send::<Job>(); // fine
assert_send::<Cached>(); // error[E0277]: `Rc<String>` cannot be
// sent between threads safely
}The error message even walks the field chain ("required because it appears within the
type Cached"). The reverse direction needs unsafety: leaf types that wrap a
raw pointer or do un-synchronised interior mutation opt out, and everything
containing them loses the trait. The opt-outs in std are written as negative impls
(impl !Send for Rc<T> — the negative_impls feature,
unstable for user code); raw pointers *const T / *mut T are
neither Send nor Sync by default as a deliberate speed bump for unsafe authors.
3 · Why Rc isn't Send — and Arc is
Rc<T> keeps its strong/weak counts as plain Cell<usize>
in the heap block shared by all clones. clone() is a non-atomic
read-modify-write. Hand one clone to another thread and two threads now increment the
same counter without synchronisation — a textbook data race, lost updates, double-free or
leak. So Rc is neither Send (moving a clone moves access to the shared
counter) nor Sync (&Rc lets you call clone).
use std::rc::Rc;
use std::thread;
fn main() {
let shared = Rc::new(vec![1, 2, 3]);
let clone = shared.clone();
thread::spawn(move || { // error[E0277]:
println!("{:?}", clone); // `Rc<Vec<i32>>` cannot be sent
}); // between threads safely
}
// note: the trait `Send` is not implemented for `Rc<Vec<i32>>`
// help: use `Arc` insteadArc<T> makes the counts AtomicUsize and becomes eligible
— but conditionally: Arc<T>: Send requires
T: Send + Sync. Both bounds are necessary and each guards a different attack.
T: Sync because clones on different threads deref to &T
simultaneously. T: Send because whichever thread drops the last clone
drops the T — through an Arc you can effectively move a value to another thread.
Drop T: Send and you could smuggle an Rc across threads inside
an Arc.
4 · The std table — and the two clever rows
| Type | Send | Sync | Why |
|---|---|---|---|
&T | iff T: Sync | iff T: Sync | sharing a reference is what Sync means |
&mut T | iff T: Send | iff T: Sync | an exclusive ref is as good as ownership |
Rc<T> | no | no | non-atomic refcounts |
Arc<T> | iff T: Send + Sync | iff T: Send + Sync | shared deref + last-clone drop |
Cell / RefCell<T> | iff T: Send | no | unsynchronised interior mutation |
Mutex<T> | iff T: Send | iff T: Send | see below |
MutexGuard<'_, T> | no | iff T: Sync | see below |
*const T / *mut T | no | no | deliberate speed bump for unsafe code |
Row one to internalise: Mutex<T>: Sync requires only
T: Send — not T: Sync. Sharing
&Mutex<T> across threads never produces two simultaneous
&T: the lock guarantees exclusive access, so what's actually happening is
serialised hand-offs of the value between threads — and "may be handed between threads" is
exactly T: Send. This is why Mutex<Cell<u32>> is
perfectly fine to share: the mutex manufactures Sync from Send. (RwLock
needs T: Send + Sync for Sync, because multiple readers hold
&T concurrently.)
Row two: MutexGuard is not Send, whatever T is. On pthread
platforms, unlocking a mutex from a thread that didn't lock it is undefined behaviour, so
the guard — whose destructor unlocks — must die on the thread that created it. This is
the real reason holding a std::sync::MutexGuard across an .await
fails to compile on multi-threaded executors: the suspended future could resume on
another worker thread carrying the guard with it.
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
impl<T: ?Sized> !Send for MutexGuard<'_, T> {}
unsafe impl<T: ?Sized + Sync> Sync for MutexGuard<'_, T> {}5 · Where the bounds bite: spawn
Auto-traits earn their keep at API boundaries. The two you hit weekly:
// std::thread
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static;
// tokio (multi-threaded runtime)
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static;A closure or future is a struct of its captures, so the auto-trait rule applies to it
like any other type: capture an Rc, the closure isn't Send, the spawn won't
compile. For futures it's sharper — the future type contains every local held
across an await point, so an Rc created and dropped between awaits
is fine, while one held across an await makes the whole future !Send. That
is the entire decoding key for "future cannot be sent between threads safely": the
compiler names the offending local and the await it lives across. (The
ownership-meets-async page
works through these errors in detail.)
6 · PhantomData and steering auto-traits without unsafe
PhantomData<T> is a ZST that tells the type system "act as if I
contain a T" — for variance, for dropck, and for auto-traits. Since auto-traits are
inferred from fields, a phantom field is the stable, safe way to steer them:
use std::marker::PhantomData;
/// Handle bound to the thread that created it (e.g. a GUI or OpenGL handle).
/// PhantomData<*const ()> is neither Send nor Sync, so neither is Handle —
/// no unsafe, no nightly negative impls needed.
pub struct Handle {
id: u32,
_not_send: PhantomData<*const ()>,
}
/// FFI wrapper that owns a C object behind a raw pointer.
/// The phantom also tells dropck and variance we logically own a T.
pub struct CBox<T> {
ptr: *mut T,
_owns: PhantomData<T>,
}The conventional recipes: PhantomData<*const ()> for !Send + !Sync;
PhantomData<Cell<()>> for Send-but-!Sync. (Nothing useful is
!Send-but-Sync; MutexGuard is the rare legitimate case and needs the nightly
negative impl.) Crates like winit and rusqlite use exactly this
to keep OS-thread-affine handles off other threads at compile time.
7 · unsafe impl Send — the contract you're signing
When you wrap a raw pointer (FFI handle, mmap region, lock-free node) your type loses both traits even if it's actually fine. The override is a one-liner with a heavy contract:
/// Owned region returned by mmap. We are the unique owner; the kernel
/// doesn't care which thread calls munmap.
pub struct Mapping {
ptr: *mut u8,
len: usize,
}
// SAFETY: Mapping uniquely owns the region (no aliases are ever handed
// out except via &self/&mut self with normal borrow rules), and unmapping
// from any thread is allowed. Reads through &self are of immutable bytes.
unsafe impl Send for Mapping {}
unsafe impl Sync for Mapping {}
impl Drop for Mapping {
fn drop(&mut self) {
unsafe { libc::munmap(self.ptr.cast(), self.len) };
}
}What you are asserting, precisely: for Send, that every operation reachable
through owned or &mut access stays sound when the value lives on a
different thread than the one that created it (watch for thread-affine C APIs, TLS,
OpenGL contexts). For Sync, that every operation reachable through
&self is safe to run from many threads at once — which means: no
unsynchronised interior mutability behind that shared reference. Getting it wrong is not
a bug in the ordinary sense; it deletes the language's core guarantee for every
downstream user of your type. The convention: always a // SAFETY: comment,
and prefer bounding it (unsafe impl<T: Send> Send for MyBox<T>)
so the obligation stays structural.
unsafe impl Send/Sync with no SAFETY
comment, or one on a type containing Rc, RefCell or a
non-thread-safe C handle, is the single highest-yield thing to flag in a Rust code
review. It is also the classic fix people apply to make a spawn error "go away" — the
compiler error was the correct behaviour.8 · What Send + Sync don't promise
- No deadlock freedom. Lock ordering is runtime behaviour. Two Sync mutexes acquired in opposite orders deadlock fine.
- No race-condition freedom. Atomics are Sync; a check-then-act sequence on an
AtomicUsizeis still a logical race. Send/Sync rule out data races (unsynchronised conflicting access, which is UB), not concurrency bugs. - Nothing inside
unsafe. The auto-trait machinery trusts everyunsafe impl; one wrong assertion and the proofs above it are vacuous. - No runtime cost either way. Neither trait adds code.
Arcis slower thanRcbecause of the atomics it needed to earn the traits, not because of the traits.
References
- Rustonomicon: Send and Sync — the semantics and when to unsafe impl.
- std::marker::Send / Sync — the docs, including the implementor lists.
- The Reference: auto traits — the derivation rule, normatively.
- std::sync::Mutex — the conditional Send/Sync impls at the bottom of the page.
- Rustonomicon: PhantomData — the recipe table.