Part 4 · Concurrency & async
`Send` and `Sync`
Two traits with no methods, derived automatically, that give Rust thread safety without a single new rule. Also: exactly what "fearless concurrency" does and does not promise.
What this lesson is trying to break (4)
- Rust prevents race conditions.
- Rust prevents deadlocks.
- `Send` and `Sync` are something you implement.
- Thread safety is a separate system from the borrow checker.
Part 2 gave you one rule about references. This part shows what happens when you extend it across threads, and the striking thing is how little has to be added.
Two marker traits. No methods. Both derived automatically.
The definitions
pub unsafe auto trait Send {} // can be MOVED to another thread
pub unsafe auto trait Sync {} // can be SHARED with another thread
Empty bodies. They carry no behaviour — they are claims about a type, checked by the compiler.
T: Send— a value ofTcan be transferred to another thread.T: Sync— a&Tcan be handed to another thread.
And the relationship that ties them together, which is worth memorising because it is asked in interviews and comes up in real discussions:
T: Syncif and only if&T: Send.
Sharing a T between threads is sending a &T to another thread. There is no
second concept — Sync is defined in terms of Send.
Auto traits
You do not implement these. The compiler does, structurally:
A type is
Sendif all of its fields areSend. A type isSyncif all of its fields areSync.
So a struct of String and u32 is both, automatically, with no annotation.
This is why the vast majority of Rust types are thread-safe without anyone
thinking about it, and why you can go a long time without meeting these traits at
all.
The interesting content is the short list of types that opt out.
Varies: only the reason the type opts out
| Type | Send? | Sync? | Why |
|---|---|---|---|
Rc<T> | No | No | Non-atomic refcount. Two threads incrementing it would race, dropping the count early → use-after-free. |
Arc<T> | Yes* | Yes* | Same design, atomic refcount. (* if T: Send + Sync.) |
RefCell<T> | Yes* | No | The borrow counter is not atomic. Moving the whole cell is fine; sharing it lets two threads borrow at once. |
Mutex<T> | Yes* | Yes* | That is its job — it makes an inner T shareable by serialising access. |
Cell<T> | Yes* | No | Same reason as RefCell: unsynchronised mutation through a shared reference. |
MutexGuard<T> | No | Yes* | Some platforms require the unlocking thread to be the locking thread. |
*const T / *mut T | No | No | The compiler cannot know what a raw pointer points at, so it assumes the worst. |
Read the Rc/Arc and RefCell/Mutex rows as pairs. Each pair is the same data structure, single-threaded and thread-safe — and the thread-safe one is slower for exactly the reason that makes it safe.
The RefCell row is the one that teaches most. It is Send but not Sync.
Moving the whole cell to another thread is fine — only one thread has it. Sharing
a &RefCell between two threads is not, because both could call borrow_mut()
simultaneously and the non-atomic counter would not notice.
Send and Sync are genuinely independent properties, and that pair is why.
The payoff
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(vec![1, 2, 3]);
let cloned = Rc::clone(&data);
let handle = thread::spawn(move || {
println!("{:?}", cloned);
});
handle.join().unwrap();
}The thread::spawn line fails:
error[E0277]: `Rc<Vec<i32>>` cannot be sent between threads safely
= help: within `{closure}`, the trait `Send` is not implemented for `Rc<Vec<i32>>`
note: required by a bound in `spawn`Now look at what actually happened. spawn’s signature is:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,The closure captured cloned, so the closure type contains an Rc. Send is an
auto trait, so the closure is not Send because one of its captures is not.
The bound fails, and the error points at the exact type responsible.
No special analysis ran. No thread-safety checker. The ordinary trait solver from Part 3 rejected an ordinary bound.
That is the whole mechanism, and it is worth appreciating: Rust’s thread safety is not a separate system bolted onto the language. It is the type system doing its normal job, given two well-chosen marker traits.
Change Rc to Arc and it compiles.
What “fearless concurrency” actually means
Rust makes concurrent programming safe. If it compiles, the concurrency is correct — no races, no deadlocks, no weird intermittent failures.
Rust prevents exactly one class of bug: data races. Deadlocks compile fine. Livelocks compile fine. Logical race conditions — two operations correct individually, wrong when interleaved — compile fine. You saw a deadlock compile in 1.4.
Say it precisely: Rust eliminates data races at compile time. A data race is unsynchronised concurrent access to one location where at least one access is a write. That is a well-defined, narrow, and genuinely valuable guarantee. It is not “concurrency is now easy”.
Being precise here is a real credibility marker. Overselling this is common, and people who write concurrent systems for a living notice immediately. The honest version is still impressive: data races are the bug class that is hardest to reproduce, hardest to debug, and most likely to be found in production rather than in tests.
What remains yours:
- Deadlocks. Lock ordering is still your responsibility.
- Race conditions. Check-then-act across two atomic operations is still a bug.
- Starvation and fairness.
- Logical correctness. Rust does not know what your program means.
unsafe impl Send
Occasionally a type is genuinely thread-safe but the compiler cannot see it — almost always because it contains a raw pointer. Then you write:
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}
unsafe because you are making a promise the compiler cannot verify. If the
promise is false, you get data races with no warning — undefined behaviour in
fully safe-looking downstream code.
When you see this in a crate, it deserves attention. Well-written code will have
a // SAFETY: comment explaining exactly why the promise holds. If it does not,
that is a legitimate review comment.
PhantomData<*const ()> · PhantomData<Cell<()>>
A deliberate opt-out of an auto trait.
Adding a zero-sized PhantomData containing a non-Send type makes the whole
struct non-Send, without adding any runtime data. *const () blocks both
Send and Sync; Cell<()> blocks only Sync.
Authors do this when a type is only valid on the thread that created it — GUI handles, thread-local resources, some FFI objects. When you see it, read: this type must not leave its thread, and the author made the compiler enforce it.
Explain, in your own words:
- Why
Mutex<Rc<T>>is notSync, even thoughMutexis normally the thing that makes something shareable. - Why
Arc<RefCell<T>>compiles but is almost always a mistake, and what the author probably meant.
Both are single sentences if you have the definitions right.
What you should now be able to say
T: Send— movable to another thread.T: Sync— shareable. AndT: Sync⟺&T: Send.- Both are auto traits: derived structurally, all the way down.
Rc/ArcandRefCell/Mutexare the same structures with and without atomicity — that is the whole difference.RefCellisSendbut notSync, which is why the two traits are separate.- Thread safety is the ordinary trait solver checking an ordinary bound, not a separate system.
- Rust eliminates data races. Deadlocks and logical race conditions remain yours.