Send and Sync: The Traits Behind Fearless Concurrency
How does the Rust compiler actually know that an `Arc<Mutex<T>>` is safe to share across threads but an `Rc<T>` isn't? The answer is two of the most elegant ideas in Rust: a pair of marker traits, `Send` and `Sync`, that encode thread-safety directly into the type system. They're rarely written by hand and often invisible, yet they're the machinery that makes fearless concurrency work — the compiler reasons about thread-safety by checking these traits, automatically, at compile time.
This post reveals the machinery behind Rust’s concurrency guarantees: the Send and Sync marker traits. They encode, in the type system, which types can be safely moved to another thread (Send) and shared between threads (Sync). The compiler uses them to check thread-safety automatically — this is how Rust catches data races at compile time. You rarely implement them yourself, but understanding them explains why the previous posts’ rules hold.
What Send and Sync are
Send and Sync are marker traits — traits with no methods, that simply mark a type as having a property. They mark thread-safety:
Send: safe to transfer ownership to another thread. A type isSendif it’s safe to move it to another thread (transfer ownership across a thread boundary). Most types areSend— you can move them into a thread (asmoveclosures do). A type that’s notSendcannot be moved to another thread.Sendmarks “safe to send to another thread.”Sync: safe to share references between threads. A type isSyncif it’s safe for multiple threads to access it via shared references (&T) simultaneously — i.e.&TisSend. A type isSyncif sharing it (by reference) across threads is safe.Syncmarks “safe to share between threads by reference.” (Roughly:Sendis about moving the value;Syncis about sharing references to it.)- They’re marker traits — no methods, just meaning.
SendandSynchave no methods — they just mark a type as thread-safe in these ways. They exist purely to let the compiler reason about thread-safety. They’re not something you call; they’re properties the compiler checks. Markers, not behavior.
Send (safe to move to another thread) and Sync (safe to share by reference between threads) are marker traits — no methods, just encoding thread-safety properties into the type system so the compiler can reason about them. They’re the vocabulary in which Rust expresses thread-safety. And remarkably, the compiler assigns them automatically.
They’re automatic (auto traits)
The elegant part: Send and Sync are auto traits — the compiler automatically implements them for types whose components are all Send/Sync, so you rarely deal with them explicitly:
- Automatically derived from components. A type is automatically
Send/Syncif all its parts (fields) areSend/Sync. The compiler propagates these traits automatically through your types — a struct ofSendfields isSend, without you writing anything. So most types areSendandSyncautomatically, and you never think about it. The compiler handles it for you. - This is why concurrency “just works” for safe types. Because
Send/Syncare automatic, ordinary safe types can be moved to and shared between threads without you doing anything — the compiler knows they’re thread-safe. This automatic propagation is why the threading in the previous posts “just worked” for normal data: those types were automaticallySend/Sync. Thread-safety is inferred, not annotated. It’s invisible when things are safe. - Non-thread-safe types opt out. Types that are not thread-safe don’t get
Send/Sync(their design opts out). The prime example:Rc<T>is notSend(its non-atomic reference counting is unsafe across threads) — so the compiler automatically knowsRccan’t cross threads, and rejects it (from the previous post).Arc<T>, with atomic counting, isSend/Sync— so it’s allowed. The traits encode exactly which types are thread-safe. Unsafe types simply aren’tSend/Sync, so the compiler rejects them.
Send and Sync are auto traits — automatically implemented by the compiler for types whose components all have them — so most types get thread-safety automatically (why concurrency “just works” for safe types), while non-thread-safe types (like Rc, whose counting isn’t atomic) don’t get them, so the compiler automatically rejects unsafe cross-thread use. This automatic mechanism is how the compiler enforces thread-safety.
How the compiler uses them
Send and Sync are the mechanism behind Rust’s compile-time concurrency safety — the compiler checks them to enforce thread-safety:
thread::spawnrequiresSend. When you move data into a thread (via amoveclosure tothread::spawn), the compiler requires that data to beSend(safe to move to another thread). If you try to move a non-Sendtype (likeRc) into a thread, the compiler rejects it — because it’s notSend. This is how the compiler catches unsafe cross-thread moves: it checks theSendbound. TheSendrequirement is a compile-time gate.- Sharing across threads requires
Sync. When data is shared (by reference) across threads, the compiler requires it to beSync(safe to share). Types that aren’tSynccan’t be shared across threads. SoSyncgates safe cross-thread sharing. The compiler checksSyncfor shared access. - This is the enforcement behind fearless concurrency. The previous posts’ guarantees — can’t send
Rcacross threads,Arc<Mutex<T>>is safe to share — are implemented viaSend/Sync: the compiler checks these traits and rejects unsafe code.Send/Syncare the machinery that makes Rust’s data-race-free-at-compile-time guarantee work. When the compiler stops a data race, it’s checkingSend/Sync. They’re the enforcement mechanism of fearless concurrency. This is how it works under the hood.
The compiler enforces thread-safety by checking Send (required to move data to a thread) and Sync (required to share data across threads) — rejecting types that lack them (like Rc). Send/Sync are the machinery behind Rust’s compile-time data-race prevention: the guarantees of fearless concurrency are implemented as trait bounds the compiler checks. This is elegant, but it’s also why you rarely touch them directly.
What this means in practice
For everyday Rust, Send/Sync mostly work invisibly — but understanding them clarifies the concurrency model, and there are a few practical points:
- You rarely implement them. Because they’re automatic, you almost never implement
Send/Syncby hand — the compiler handles it. You mostly just benefit from them (thread-safety checked for free) and occasionally encounter them in compiler errors (when you try something not-thread-safe). They work behind the scenes. You benefit without writing them. - They explain the compiler errors. When the compiler rejects your concurrent code (“
Rc<...>cannot be sent between threads safely” — i.e. notSend), it’s reporting aSend/Syncviolation. Understanding these traits makes those errors make sense — the compiler is telling you the type isn’t thread-safe. RecognizingSend/Syncin errors clarifies what’s wrong and how to fix it (e.g. useArcinstead ofRc). They demystify concurrency compile errors. - They’re the reason the guarantees hold. Understanding
Send/Syncexplains why Rust’s fearless concurrency actually works: it’s not magic, it’s a precise, checkable encoding of thread-safety in the type system, propagated automatically and enforced by the compiler. The guarantees rest on this concrete mechanism. Fearless concurrency isSend/Sync(plus ownership) checked at compile time. Now you know how it works. - They embody Rust’s philosophy.
Send/Syncexemplify Rust’s approach — encode safety properties in the type system, check them at compile time, make the safe path automatic and the unsafe path a compile error. Concurrency safety, like memory safety, is achieved through the type system, not runtime checks or programmer discipline. It’s the same philosophy that runs through all of Rust, applied to threads. Types encode safety.
Send and Sync are the marker traits — automatic and mostly invisible — that encode thread-safety in Rust’s type system: Send (safe to move to a thread), Sync (safe to share between threads). The compiler checks them to enforce data-race-free concurrency at compile time, which is the machinery behind fearless concurrency. You rarely implement them, but understanding them explains why the guarantees hold. Next: message passing — the other concurrency paradigm, using channels.
Key takeaways
SendandSyncare marker traits (no methods, just meaning) that encode thread-safety in the type system: a type isSendif it’s safe to move to another thread, andSyncif it’s safe to share by reference (&T) between threads — roughly,Sendis about moving the value,Syncabout sharing references to it.- They’re auto traits — the compiler automatically implements them for types whose components are all
Send/Sync— so most types get thread-safety automatically (why concurrency “just works” for ordinary safe types), while non-thread-safe types opt out (e.g.Rcisn’tSendbecause its reference counting isn’t atomic, whereasArcis). - The compiler enforces thread-safety by checking these traits:
thread::spawnrequires the moved data to beSend(rejectingRc), and sharing across threads requiresSync— soSend/Syncare the machinery implementing Rust’s compile-time data-race prevention (when the compiler stops a data race, it’s checking these traits). - In practice you rarely implement
Send/Sync(they’re automatic) — you mostly benefit from them (free thread-safety checking) and encounter them in compiler errors (like “cannot be sent between threads safely”), which understanding them makes sensible (the fix is often usingArcinstead ofRc). Send/Syncexplain why fearless concurrency works — it’s not magic but a precise, checkable, automatically-propagated encoding of thread-safety in the type system, enforced at compile time — embodying Rust’s philosophy of achieving safety (memory and concurrency alike) through the type system rather than runtime checks or programmer discipline.
Further reading
- The Rust Book — Extensible Concurrency with Sync and Send
- Shared state: Arc and Mutex (previous post)
- Rust: Traits (Module 2)