Thread Safety Traits: The Send & Sync Formalisms
Master the mathematical relationship between Send and Sync, auto-trait derivation rules, non-thread-safe types, and compile-time data race prevention.
In C and C++, thread safety is an undocumented convention: developers must remember which objects can be passed across threads and which pointers must be guarded by locks. A single mistake triggers silent data races.
In Rust, thread safety is formally proven at compile time through two built-in marker traits: Send and Sync.
A type T is thread-safe to share if and only if an immutable reference to T can be safely sent across threads.
1. Defining Send and Sync#
Unlike normal traits that define methods, Send and Sync are marker traits: they have zero methods and exist purely to convey capabilities to the compiler.
| Marker Trait | Capability Conferred | Core Question Answered |
|---|---|---|
Send | Safe to transfer ownership across a thread boundary. | Can I move this value into another thread? |
Sync | Safe to share references (&T) across multiple threads simultaneously. | Can multiple threads read this value concurrently? |
The Equivalence Theorem#
The relationship between Send and Sync is formalized by a fundamental equivalence:
A type is Sync if and only if a shared immutable reference to () implements Send.
2. Auto-Trait Derivation Rules#
Send and Sync are auto traits: the compiler automatically implements them for your custom structs and enums if all of their constituent fields implement them:
For a compound struct containing fields :
If a struct contains even a single field that is not thread safe (such as an Rc pointer or raw pointer), the entire struct automatically loses Send or Sync.
3. The Anatomy of Non-Thread-Safe Types#
Understanding why certain types are marked !Send or !Sync illuminates how Rust eliminates concurrency bugs:
Why Rc<T> is !Send and !Sync#
Rc<T> uses standard, non-atomic CPU instructions (count += 1) to increment its reference count. If two threads cloned an Rc concurrently, a CPU data race would corrupt the counter, causing premature memory deallocation and use-after-free bugs!
12345678910111213use std::rc::Rc; use std::thread; fn main() { let rc_value = Rc::new(42); // COMPILE ERROR: `Rc<i32>` cannot be sent between threads safely // thread::spawn(move || { // println!("{}", rc_value); // }); // SOLUTION: Use Arc<T> instead of Rc<T>! }
4. Concept Verification#
Test your understanding of Send and Sync invariants:
What is the formal definition of the Sync trait in relation to Send?
Why does containing a raw pointer *const u8 make a struct automatically !Send and !Sync?