Reading Rust

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.

40 mincontrasting casesPOErefutation text
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 of T can be transferred to another thread.
  • T: Sync — a &T can 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: Sync if 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 Send if all of its fields are Send. A type is Sync if all of its fields are Sync.

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

TypeSend?Sync?Why
Rc<T>NoNoNon-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*NoThe 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*NoSame reason as RefCell: unsynchronised mutation through a shared reference.
MutexGuard<T>NoYes*Some platforms require the unlocking thread to be the locking thread.
*const T / *mut TNoNoThe 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

Predict firstcommit in writing, then look
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();
}

What “fearless concurrency” actually means

Unlearn thiscarried over from marketing copy, and from people who repeat it without checking
You think

Rust makes concurrent programming safe. If it compiles, the concurrency is correct — no races, no deadlocks, no weird intermittent failures.

Wrong here

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.

Instead

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.

Beaconsee this → think this, without decoding it

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 it to yourselfwrite it — thinking it does not count

Explain, in your own words:

  1. Why Mutex<Rc<T>> is not Sync, even though Mutex is normally the thing that makes something shareable.
  2. 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. And T: Sync&T: Send.
  • Both are auto traits: derived structurally, all the way down.
  • Rc/Arc and RefCell/Mutex are the same structures with and without atomicity — that is the whole difference.
  • RefCell is Send but not Sync, 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.

Next: 4.2 — Threads, channels, and shared state.

In the wildgo read the real thing now, while it is fresh
  • std::marker::SendNote that the trait body is empty and the implementors list is essentially "almost everything". Then look at the short list of types that are NOT Send — that list is the actual content.
  • Rc — why it is not SendSearch for `impl<T: ?Sized> !Send for Rc<T>`. A negative impl, written deliberately. Compare with Arc in the same file, which uses atomics and is Send. The two types differ in exactly one thing.
Scheduled for recall4 items added to your review queue

These are not shown to you now on purpose. They will surface in Review tomorrow, mixed in among items from other parts of the course — not grouped by topic. Sorting out which idea applies is the part that transfers.