Send Moves the Value, Sync Shares the Reference: The Exact Difference
Before Rust compiles a thread::spawn or a channel send, it does not audit your logic — it asks one question about types: does this type implement the right marker trait? Send and Sync are auto traits: the compiler implements them for your struct by inspecting its fields, you write no method, and they occupy zero bytes with no dynamic dispatch. They exist purely as compile-time contracts that the standard library's concurrency APIs bound on.
The two definitions are one line each, and the precision matters:
T: Send— ownership of aTcan be transferred to another thread. Moving aTinto a spawned closure is safe; the value itself travels.T: Sync— a&Tcan be handed to another thread. The exact formulation interviewers want:T: Syncif and only if&T: Send.
Notice what each is not. Send says nothing about copying — it is about moving an owned value, so exactly one thread ends up with it. Sync says nothing about T moving at all; it is about many threads holding references to one value simultaneously. Vec<String> is both: you can move a vector to a worker, and you can hand &Vec<String> to several workers at once.
The interesting types are the ones that fail:
| Type | Send |
Sync |
Why |
|---|---|---|---|
i32, String, Vec<u8> |
yes | yes | plain data, no shared mutable state |
Rc<T> |
no | no | non-atomic count would race on clone and drop from two threads |
Cell<T> / RefCell<T> |
yes | no | interior mutation reachable through &T with no synchronization |
*const T / *mut T |
no | no | the compiler cannot prove your aliasing discipline |
Mutex<T> (where T: Send) |
yes | yes | the lock serializes every access |
You can ask the compiler directly, which makes a good sanity check in a scratch binary:
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<String>();
assert_sync::<String>();
assert_send::<std::rc::Rc<i32>>(); // E0277: `Rc<i32>` cannot be sent between threads safely
assert_sync::<std::cell::RefCell<i32>>(); // E0277: `RefCell<i32>` cannot be shared between threads safely
The rule propagates through closures too: a closure is Send exactly when every value it captures is Send, so a move closure holding an Rc fails not because of anything the body does but because of one captured variable. That is why the error points at the closure rather than at your logic — the compiler is auditing the capture list, and swapping that field for an Arc is the entire fix.
Those bounds are what std's APIs are actually saying. thread::spawn requires the closure to be Send + 'static; an mpsc channel requires its message T: Send to cross; Arc<T> only becomes Send/Sync when T: Send + Sync. So when you move a closure into a thread and see this, read it as a rejected design:
error[E0277]: `Rc<RefCell<i32>>` cannot be sent between threads safely
= note: required for `Rc<RefCell<i32>>` to implement `Send`
= note: required for the closure passed to `std::thread::spawn` to implement `Send`
= help: the trait `Send` is not implemented for `Rc<RefCell<i32>>`
One subtlety worth memorizing: Arc<RefCell<i32>> fails differently. Because Arc<T> needs T: Sync to become Send itself, the error blames sharing, not transfer — `RefCell<i32>` cannot be shared between threads safely. Same broken program, different trait in the message, and the message tells you which layer to fix. That distinction is the working definition of the pair: Send moves a value, Sync shares a reference, and the reason Arc and Mutex for Shared State works is that its trait arithmetic adds up.
Auto Traits, Negative Impls, and Why Mutex Makes T Sync
Nobody writes impl Send for String. Auto traits are derived structurally: a type is Send when every field is Send, Sync when every field is Sync, and generics inherit the rule — Vec<T>: Send exactly when T: Send, HashMap<K, V> when both K and V are. One untracked field poisons the whole struct, which is why the compiler prints a field-by-field note chain instead of a single verdict.
The interesting implementations are the negative ones. The standard library explicitly opts types out with impl !Send / impl !Sync: Rc is neither (its count would race), Cell and RefCell are !Sync (mutation reachable through a shared reference), and raw pointers are neither because a *mut T carries an address with no evidence about who else aliases it. On stable Rust a crate cannot declare a negative impl, so the standard trick for making your own type refuse threads is to hold a field whose type is already !Send + !Sync:
use std::marker::PhantomData;
/// Owns a handle that is only valid on the thread that created it.
struct ThreadBoundHandle {
id: u32,
_thread_local: PhantomData<*mut ()>, // *mut () is !Send + !Sync, so this struct inherits it
}
Now the sentence this topic hangs on: unsafe impl<T: Send> Sync for Mutex<T> — note what the bound does not require. T: Sync is missing, and deliberately so. Holding only &Mutex<T> you can call lock() and obtain an exclusive &mut T; you can never observe two shared references to the guarded value at the same time. The lock supplies the synchronization RefCell lacks, so the only thing that must hold is that the value can be moved in and out of the lock — T: Send.
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
fn assert_sync<T: Sync>() {}
assert_sync::<Mutex<RefCell<i32>>>(); // compiles: the lock serializes access
// assert_sync::<RefCell<i32>>(); // E0277: `RefCell<i32>` cannot be shared between threads safely
let good: Arc<Mutex<i32>> = Arc::new(Mutex::new(0)); // Send + Sync, crosses spawn
// let bad: Arc<RefCell<i32>> = Arc::new(RefCell::new(0)); // E0277: RefCell is not Sync
That is the whole mechanism behind the default shared-mutable pattern. RefCell<i32> is !Sync, wrap it in a Mutex and the combination is Sync — then Arc on top gets its Send + Sync and Arc<Mutex<RefCell<i32>>> legally crosses a thread boundary. The bounds stack like this:
| Type | Send requires |
Sync requires |
|---|---|---|
Arc<T> |
T: Send + Sync |
T: Send + Sync |
Mutex<T> |
T: Send |
T: Send |
RwLock<T> |
T: Send |
T: Send + Sync |
RefCell<T> |
T: Send |
never — opted out |
Rc<T> |
never — opted out | never — opted out |
RwLock demands more than Mutex for its Sync bound, and the reason is worth seeing once: readers collect &T (so T must be Sync) while the writer gets &mut T (so T must be Send), and both are reachable from a shared &RwLock<T> — moving the whole lock, by contrast, needs only T: Send. The same field-by-field reading tells you why Vec<Mutex<i32>> is fine — Mutex<i32> is Send + Sync, so the vector inherits both.
Read the table as arithmetic and the roadmap's pattern falls out: Arc<Mutex<T>> is the shared-mutable pattern because any T: Send makes the entire stack Send + Sync, which is exactly what thread::spawn, scoped threads, and channel messages bound on. When a design fails, the failure arrives as one of those compile errors at the boundary — which is what the next chapter teaches you to read.
Reading E0277 at the Thread Boundary: Fixing a Type That Will Not Cross
Here is the program every Rust engineer writes once. An AppState cache is shared with Rc<RefCell<..>> in the single-threaded version, and now a worker has to run in parallel:
use std::cell::RefCell;
use std::rc::Rc;
use std::thread;
let state = Rc::new(RefCell::new(vec![0_u8; 1024]));
thread::spawn(move || {
let mut s = state.borrow_mut();
s[0] = 1;
});
error[E0277]: `Rc<RefCell<Vec<u8>>>` cannot be sent between threads safely
--> src/main.rs:7:20
|
7 | thread::spawn(move || {
| ^^^^^^^ closure may outlive the current function...
| required for `Rc<RefCell<Vec<u8>>>` to implement `Send`
|
= help: the trait `Send` is not implemented for `Rc<RefCell<Vec<u8>>>`
= note: within `Rc<RefCell<Vec<u8>>>`, the trait `Sync` is not implemented ...
Read it in three steps rather than as a wall of text. One: which trait? Send in the headline means the compiler believes a value would cross a thread; the word shared (Sync) means a reference would. Two: which type broke? Follow the note chain inward — it names the innermost offender first (Rc or RefCell here), then lists each enclosing requirement that depended on it. Three: fix the type or fix the design? The notes are a dependency graph, not a suggestion.
| The note blames | What it means | The fix |
|---|---|---|
Rc<T> |
non-atomic shared count | Arc<T> — same API, atomic count |
RefCell<T> / Cell<T> |
unsynchronized interior mutation | Mutex<T> / RwLock<T>, or stop sharing it |
*const T / *mut T |
hand-rolled aliasing, unproven | wrap it in a type with a real invariant |
| a field of your struct | one field poisons the struct | replace that field, not the whole type |
Applying the table to the program above: Rc becomes Arc, RefCell becomes Mutex, and it compiles — which is precisely Arc and Mutex for Shared State in one line of repair. The diagnostic never asks you to redesign; it asks you to name which single field broke the structural rule.
Sometimes you will conclude the type genuinely must not cross threads — it owns an OS handle bound to the creating thread, or a windowing context. You have two honest options. The safe one is the PhantomData<*mut ()> field from the previous chapter, which opts out structurally and lets the compiler keep refusing. The unsafe one is unsafe impl Send for MyType {}, and the keyword is doing real work: you are promising the compiler that transferring ownership of this value cannot cause a data race, and nothing will check your promise. Get it wrong and a compile error silently becomes a race in production, so the bar is a written invariant — "the handle is re-bound to the receiving thread before first use" — not a hunch.
The same bounds resurface outside std::thread. Tokio's spawn requires Future + Send + 'static, so a !Send handle (or a guard held across .await) makes the future fail the same way a closure did — the diagnostics look different, the mechanism is identical. That is the bridge to async fn and await.
For an interview, the answer that scores is short and exact: Send means ownership can move to another thread; Sync means &T can be shared with other threads (T: Sync iff &T: Send); both are auto traits derived from fields; Mutex adds Sync by serializing access, which is why Arc<Mutex<T>> is the default shared state type — plus one example each of a type that is neither (Rc) and one that is Send but not Sync (RefCell).
Quiz
1. Which sentence states the difference between Send and Sync precisely?
2. Why is RefCell<i32> Send but not Sync?
3. std declares unsafe impl<T: Send> Sync for Mutex<T>. Why does the bound require T: Send but not T: Sync?
4. Moving Arc<RefCell<i32>> into thread::spawn fails with 'RefCell<i32> cannot be shared between threads safely'. What is the root cause?
Flashcards
Question
State Send and Sync precisely, including the one-sentence equivalence between them.
Click to reveal answer
Answer
T: Send means ownership of a T can be transferred to another thread — the value travels and exactly one thread ends up owning it. T: Sync means a &T can be handed to another thread, equivalently T: Sync if and only if &T: Send. Both are auto traits derived from a type's fields, zero-sized, with no method to write and no runtime cost.
Question
Which common types are !Send or !Sync, and what is the safety reason in each case?
Click to reveal answer
Answer
Rc<T> is neither: its non-atomic strong count races on clone/drop from different threads. Cell/RefCell are Send but !Sync: moving one keeps a single owner, but sharing &RefCell would let two threads borrow_mut without synchronization. Raw pointers *const T/*mut T are neither: an address carries no proof about aliasing, so the compiler cannot vouch for them. All are opted out by negative impls in std.
Question
Why does Mutex<T> implement Sync for T: Send without requiring T: Sync?
Click to reveal answer
Answer
Sync means &T is safe to share. From only a &Mutex<T>, lock() yields an exclusive &mut T, so concurrent holders never observe shared references to the value — the lock supplies the synchronization RefCell lacks. The remaining requirement is moving the value in and out, which is T: Send. Result: Mutex<RefCell<i32>> is Send + Sync even though RefCell<i32> is !Sync.
Question
What Send/Sync bounds do Arc, Mutex, and RwLock impose, and what pattern falls out?
Click to reveal answer
Answer
Arc<T> is Send + Sync only if T: Send + Sync. Mutex<T> is Send + Sync whenever T: Send. RwLock<T> is Send when T: Send and Send + Sync only when T: Send + Sync, because readers get &T (needs Sync) while the writer gets &mut T (needs Send). So Arc<Mutex<T>> crosses thread::spawn for any T: Send — which is why Arc<Mutex<T>> is the default shared-mutable type.
Question
How do you make your own type refuse threads on stable Rust, and when is unsafe impl Send justified?
Click to reveal answer
Answer
Stable Rust cannot declare negative impls, so hold a PhantomData<*mut ()> field: raw pointers are !Send/!Sync and the struct inherits the opt-out. unsafe impl Send or Sync is the opposite move — you assert a safety invariant (for example the handle is re-bound to the receiving thread before first use) that the compiler will never check, so a wrong impl turns a compile error into a data race.
Revision Notes
Key Takeaways
- 1. Send is about moving ownership to another thread; Sync is about sharing references — T: Sync exactly when &T: Send — and both are auto traits derived from a type's fields at zero runtime cost
- 2. Auto traits propagate structurally, so a single !Send or !Sync field poisons the whole type; std opts Rc out entirely and Cell/RefCell out of Sync with negative impls
- 3. Mutex implements Sync for T: Send without needing T: Sync, because lock() upgrades a shared &Mutex into an exclusive &mut T — this is the mechanism that makes Arc<Mutex<T>> work
- 4. Arc requires T: Send + Sync, Mutex requires T: Send, RwLock requires both — the arithmetic is why Arc<Mutex<T>> crosses thread::spawn for any Send payload while Arc<RefCell<T>> cannot
- 5. An E0277 diagnostic names the trait, then the innermost offending type, then the chain of requirements — read it as a dependency graph and fix the field the notes blame, not the whole design
Interview Tips
- • "What is the difference between Send and Sync?" — one sentence each plus the equivalence: Send moves a value, Sync shares a reference, T: Sync iff &T: Send; then give one type that is both, one that is neither (Rc), and one in between (RefCell)
- • "Why is Arc<Mutex<T>> the shared state type?" — walk the arithmetic: T: Send makes Mutex Send + Sync, which makes Arc Send + Sync, which satisfies spawn's Send + 'static bounds — the trait system checking the design for you
- • "How would you make a type that must never cross threads?" — PhantomData<*mut ()> on stable, since negative impls are unstable; mention unsafe impl !Send exists but is library-only, so structural opt-out is the answer interviewers expect
- • "You implemented Send yourself — what did you guarantee?" — that transferring ownership cannot cause a data race, stated as an invariant (re-bind to the receiving thread before first use, or thread-affine handle with checked access); emphasize that no compiler check exists and a wrong impl converts a compile error into a production race
Cheat Sheet
Send and Sync Marker Traits
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<String>();
assert_sync::<String>();
assert_sync::<Mutex<RefCell<i32>>>(); // lock serializes; RefCell's !Sync is neutralized
// assert_send::<Rc<i32>>(); // E0277: `Rc<i32>` cannot be sent between threads safely
// assert_sync::<RefCell<i32>>(); // E0277: `RefCell<i32>` cannot be shared between threads safely
| Type | Send | Sync | Reason |
|---|---|---|---|
i32, String, Vec<u8> |
yes | yes | plain data |
Rc<T> |
no | no | non-atomic count races (negative impl) |
Cell<T> / RefCell<T> |
yes | no | interior mutation behind &T, unsynchronized |
*const T / *mut T |
no | no | address proves nothing about aliasing |
Mutex<T> (T: Send) |
yes | yes | lock turns &Mutex into exclusive &mut T |
| Bound | Requires |
|---|---|
Arc<T> Send/Sync |
T: Send + Sync |
Mutex<T> Send/Sync |
T: Send |
RwLock<T> |
Send: T: Send; Sync: T: Send + Sync (readers get &T, writer gets &mut T) |
Reading E0277: which trait (Send = value moves / Sync = reference shared) -> innermost type blamed in the notes -> fix that field or revisit the design. Arc<Mutex<T>> is the shared-mutable default because any T: Send makes the whole stack Send + Sync.
Opt out on stable: hold PhantomData<*mut ()>. Opt in unsafely: unsafe impl Send requires a written invariant you guarantee.