You have now met Send and Sync three times: as pure type-system facts, as
the errors that stop an Rc or a RefCell crossing a thread boundary, and as
something you wrote by hand with unsafe impl to make a raw pointer
shareable. This article closes the loop, now that you have atomics and can see
what the promise actually rests on.
The two declarations
pub unsafe auto trait Send {}
pub unsafe auto trait Sync {}
Three words, each load-bearing.
trait — but with no methods. Nothing to call. The entire content is a
fact the compiler uses to decide what your code is allowed to do.
auto — the compiler implements it for you, structurally. A type is
Send if all of its fields are Send. You do not write these impls and
should not want to; the derivation is automatic and correct for every type
built out of ordinary parts.
unsafe — implementing one by hand is a promise. A wrong Send impl is
not a type error, it is a data race: undefined behaviour, which on a good
day is a wrong number and on a bad day is memory corruption discovered weeks
later in a different subsystem.
The definitions, once more, in the form worth memorising:
-
T: Send— ownership of aTmay be transferred to another thread. -
T: Sync— a&Tmay be shared with another thread. Equivalently and exactly:T: Syncif and only if&T: Send.
Two derived rules fall out and are worth having: &mut T: Send iff T: Send,
and both &T and &mut T are Sync iff T is Sync.
The moment a type loses both
Store a *const T, a *mut T or a NonNull<T> in a struct and the automatic
derivation stops: raw pointers are neither Send nor Sync, so your struct
is neither either.
This is worth stating clearly because the std documentation for Send has
historically been read as implying otherwise. It is not so — the Nomicon is
explicit, and the compiler agrees. A struct containing a bare *mut u8 fails
E0277 at a spawn boundary exactly as Rc does. Trust the error message.
The rule is deliberately conservative and correct: a raw pointer carries no information about who else can reach the thing it points at, so the compiler assumes the worst. Which is where you come in.
Writing the impl, and getting the bound right
The canonical form is not the bare one:
unsafe impl<T: Send> Send for MyBox<T> {}
unsafe impl<T: Sync> Sync for MyBox<T> {}
Getting that bound wrong is how an Rc escapes to another thread. Write
the unbounded version —
unsafe impl<T> Send for MyBox<T> {} // wrong, and it compiles
— and now MyBox<Rc<Data>> is Send. Move it to another thread, clone the
Rc on both sides, and two threads race on a non-atomic refcount. Use-after-
free, or a leak, depending on which increment was lost. Your unsafe impl
did that, in a program with no other unsafe code anywhere.
The rule of thumb: your wrapper should require of T whatever the operation
it enables requires. If your type gives other threads ownership of the T,
require T: Send. If it gives them shared access, require T: Sync. If you
cannot state which, you do not yet have the proof.
The mirror image — opting out — has no stable syntax. impl !Send for X {}
is nightly-only (negative impls). The stable workaround is to include a
PhantomData<*const ()> field, which drags !Send and !Sync in
structurally. It is a hack and it is what everyone does.
The instructive case: MutexGuard is Sync but not Send
impl<T: ?Sized + Sync> Sync for MutexGuard<'_, T> {}
// and no Send impl at all
Read that twice. The guard can be shared between threads by reference, and cannot be moved to another thread.
Why: on POSIX platforms, pthread_mutex_unlock must be called by the thread
that locked it. The guard’s destructor does the unlocking. So the guard must
die on the thread that created it, and “may be moved to another thread” is
precisely what Send would license.
Notice what kind of constraint that is. It is a portability constraint
imposed by the C library underneath, not a soundness necessity on every
platform — some implementations would be perfectly happy. std takes the
strictest common denominator, because a type’s Sendness is part of its
public API and cannot be platform-dependent without splitting the ecosystem.
The consequences ripple further than you would expect:
-
you cannot
moveaMutexGuardinto a spawned thread; -
you cannot hold one across an
.await, because a future holding it would not beSend, and most executors needSendfutures.clippy::await_holding_lockwarns about exactly this, and it is the bridge into the async track; -
MappedMutexGuard— the guard that lets you project into a field of the protected data — is still unstable, which is one of the most-cited reasons people reach forparking_lot.
💡Why can this course not set a problem asserting "Rc<u32> is not Send"?
click to reveal
Because the assertion is a compile failure, and the grader runs code.
The idiomatic way to assert a marker-trait fact is a static assertion helper:
fn assert_send<T: Send>() {}
assert_send::<u32>(); // compiles
assert_send::<Rc<u32>>(); // E0277 — the program no longer builds
The positive form is gradeable. The negative form has no passing shape: a
submission containing it does not compile, so it cannot pass a test, and a
submission omitting it proves nothing. Grading this properly needs a
must_not_compile test kind — compile the file plus an appended snippet and
assert a specific error code — which this harness does not have.
There is no runtime escape either. Auto-trait membership is erased before
codegen; there is no is_send::<T>() -> bool, and writing one would require
specialisation, which is not stable.
One practical note, since older advice claims otherwise: on clippy 0.1.95 the
assert_send helper above is accepted. clippy::extra_unused_type_parameters
deliberately skips functions with an empty body precisely so the static-
assertion idiom stays legal. Add a single statement to the body and the lint
fires. So the helper is available to you — it is only the negative assertion
that has no home here.
What the tools will and will not do
-
clippy::non_send_fields_in_send_tyflags anunsafe impl Sendon a type with non-Sendfields — exactly the case you want reviewed. It is nursery, so off by default. -
clippy::undocumented_unsafe_blockswould force a// SAFETY:comment on every unsafe block. It is restriction, also off by default. -
clippy::arc_with_non_send_synccatchesArc::new(x)wherexis neitherSendnorSync— a real bug, since such anArccan never cross a thread boundary and you have paid for atomic refcounting for nothing. This one is on by default. -
clippy::await_holding_lockcatches guards held across.await.
So on the safe side of the line the compiler is doing enormous work for you, and on the unsafe side the defaults have almost nothing to say. That asymmetry is the honest summary of Rust’s guarantee: it is airtight where you have not taken the wheel, and entirely yours where you have.
The checklist for unsafe impl Send
Before you write one, be able to answer all four:
- What exactly is being shared or transferred, and through what field?
- What synchronises access to it — a lock, an atomic, or a structural argument like disjoint index ranges?
-
What bound does
Tneed, and why that one rather than the other? -
Can you write the
// SAFETY:comment? If not, you do not have the proof — you have a hope.