Send and Sync describe different ways a value can cross a thread boundary.
Most code relies on the compiler to infer these traits. Low-level abstractions
sometimes implement them manually, which requires a safety argument for the
entire type and its API.
Transfer a value or share a reference
T: Sendmeans a value of typeTcan be transferred to another thread.T: Syncmeans shared references toTcan be transferred between threads. Equivalently,&T: Sendexactly whenT: Sync.
These are separate properties. Cell<i32> is Send: one thread can hand
ownership to another. It is not Sync: concurrent access through shared
references would expose unsynchronized mutation.
For references, the relevant rules are:
| Reference type | Send when | Sync when |
|---|---|---|
&T |
T: Sync |
T: Sync |
&mut T |
T: Send |
T: Sync |
Sharing a reference to an &mut T does not give every recipient permission
to mutate T. Mutation still requires exclusive access to the mutable reference.
What auto and unsafe mean
Send and Sync have no methods. They are marker traits used in bounds,
including the bounds on thread creation and synchronization APIs.
As auto traits, they are normally inferred from a type's components. A field that lacks a trait can prevent automatic implementation for the containing type. Explicit implementations and restrictions also participate in this process; inference is not a complete proof of a custom unsafe abstraction.
Raw pointers and NonNull<T> implement neither trait. Merely storing an
address does not tell the compiler who owns the pointee, what aliases exist,
or which operations are allowed on another thread. Rc<T> is also neither
Send nor Sync because its shared reference count is not synchronized.
A wrapper may establish stronger invariants than its fields express. For
example, a uniquely owning pointer wrapper with Box-like access and cleanup
might justify Send when T: Send and Sync when T: Sync. Those bounds
are appropriate only if the wrapper's implementation actually supports that
argument. A borrowed pointer wrapper, foreign handle or synchronized owner
may need different bounds or may need to remain confined to one thread.
An unsafe impl asks the compiler to trust that argument. An incorrect
implementation can let safe callers trigger undefined behavior, including
data races, invalid access or incorrect resource cleanup. Not every mistake
manifests as a data race, and passing tests does not establish soundness.
Do not omit the pointee's requirements
Suppose a custom owner gives access to a stored T. If it implements Send
for every T, it may permit moving an Rc to another thread while another
Rc handle remains on the original thread. Both threads could then update the
same unsynchronized reference count.
A bound such as T: Send excludes that case for a Box-like owner. It does not
prove everything else: allocation ownership, aliasing, methods, destruction
and any foreign-library thread restrictions still need review.
To keep a type neither Send nor Sync on stable Rust, a marker field such as
PhantomData<*const ()> can express that restriction without storing a real
pointer. Direct negative implementation syntax, such as impl !Send for X,
is not available in ordinary stable Rust code. The marker deliberately affects
both traits, so check that this matches the API you want.
Why a mutex guard can be shared but not moved
std::sync::MutexGuard<'_, T> is not Send. It is Sync when T: Sync, so a
shared reference to such a guard can be sent to a scoped worker for reading.
The guard itself stays on the thread responsible for dropping it.
This preserves platforms' requirement that the thread unlocking a mutex be the one that locked it. The standard library chooses this portable contract; it is not evidence that trait implementations can never differ by platform.
Holding a guard across .await is not universally a compilation error.
A future that retains this guard across a suspension point is not Send, so
it cannot be used where a Send future is required. A local executor may
accept it. Even then, retaining a synchronous lock while suspended can block
progress or contribute to deadlock. Prefer ending the guard's scope before
awaiting unless the design specifically requires otherwise.
Check trait claims at compile time
A small helper can verify a positive bound:
use std::cell::Cell;
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn main() {
assert_send::<Cell<i32>>();
assert_send::<String>();
assert_sync::<String>();
}
Replacing the first call with
assert_sync::<Cell<i32>>() produces a compile error, which is the expected
evidence that shared Cell access is not supported.
A test system can treat compilation failure as success when testing a
prohibited use. Rust documentation tests marked compile_fail and dedicated
compile-test tools support this style. Negative trait assertions can also
be expressed by specialized assertion macros; they are not fundamentally
impossible on stable Rust.
The site's ordinary submission flow expects a compiling program with runtime
results, so an intentionally failing example cannot serve as a passing
submission by itself. Positive bounds verify the compiler's view of a type,
but cannot validate the reasoning behind an unsafe implementation it trusts.
There is no general standard runtime trait-reflection function named
is_send::<T>().
Use lint feedback as a review aid
Clippy can highlight suspicious manual Send implementations, missing safety comments, Arc use with unsuitable pointee types, and guards held across await points. These checks depend on which lints are enabled and on the toolchain version. They are not soundness proofs.
In particular, Arc<T> needs appropriate Send and Sync bounds on T to cross
threads. Constructing an Arc around a non-Sync value is not itself undefined
behavior; it may simply fail to meet the intended threading requirements.
Atomic reference counting protects ownership bookkeeping, not every operation
available on the pointee.
Before writing an unsafe Send or Sync implementation, explain:
- Which values and references can cross threads through the public API.
- Who owns the storage and which aliases can remain elsewhere.
- How exclusive access or synchronization prevents invalid concurrent use.
- Which generic bounds and cleanup-thread requirements are necessary.
Write the argument alongside the implementation, then check the code against it. A clear safety comment helps review; its presence alone proves nothing.