rust-send-sync
Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub
Use when deciding whether a type is Send or Sync, or when rustc rejects a value at a thread or task boundary with "cannot be sent between threads safely", "cannot be shared between threads safely", or "future cannot be sent between threads safely". Also for auto trait rules of references and smart pointers (Arc vs Rc), Mutex vs RwLock payload bounds, why MutexGuard is not Send yet is Sync, PhantomData markers that remove Send or Sync, auto trait leakage from impl Trait and async fn, and E0321. Not for proving a manual unsafe impl Send or Sync; use `rust-unsafe`.
In this repository46 other files it gives its agents.
- AGENTS.mdAGENTS.md
- cargo-workflowsSkill
- ffi-error-progress-cancelSkill
- memory-modelSkill
- rust-android-buildSkill
- rust-async-internalsSkill
- rust-borrow-semanticsSkill
- rust-callback-boundsSkill
- rust-cliSkill
- rust-code-styleSkill
- rust-compiler-errorsSkill
- rust-copy-on-writeSkill
- rust-crate-architectureSkill
- rust-crate-releaseSkill
- rust-databaseSkill
- rust-debuggingSkill
- rust-disciplineSkill
- rust-embedded-no-stdSkill
- rust-event-loop-stateSkill
- rust-hot-pathSkill
- rust-ios-buildSkill
- rust-iterator-implSkill
- rust-jniSkill
- rust-lintsSkill
- rust-macrosSkill
- rust-native-linkingSkill
- rust-networkingSkill
- rust-observabilitySkill
- rust-panic-safetySkill
- rust-pattern-semanticsSkill
- rust-performanceSkill
- rust-pin-projectionSkill
- rust-portSkill
- rust-reviewSkill
- rust-sanitizers-miriSkill
- rust-securitySkill
- rust-serdeSkill
- rust-swift-ffiSkill
- rust-tddSkill
- rust-test-toolsSkill
- rust-type-erasureSkill
- rust-unsafeSkill
- rust-varianceSkill
- rust-wasmSkill
- uniffi-boundarySkill
- uniffi-packaging-versioningSkill
The file
We split this skill into the rules it gives an agent
11 rules. About 0 are common, 3 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 9
Every error text below comes from rustc 1.98.1, edition 2024, on aarch64-apple-darwin.
- Very specific: line 65
Read the two lines together.
- Very specific: line 73
Never add unsafe impl Send or unsafe impl Sync only to make an error in this skill go away, including on a wrapper struct around an Rc, a …
- Very specific: line 150
Put the lock inside the Arc.
- Very specific: line 255
Do not read that as "nothing derived from the guard leaves the thread".
- Very specific: line 285
Do not hold a std::sync guard across an .await.
- Very specific: line 339
Write + Send only when you intend it, and add + Sync when callers share the returned value.
- Distinctive: line 345
Write the desugared form:
- Very specific: line 357
Do not silence the async_fn_in_trait warning when callers spawn the future.
- Very specific: line 373
Read the note:. It names the value and the exact .await that traps it.
- Distinctive: line 378
Use these skills by name when they are installed.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Rust Send and Sync
Every error text below comes from rustc 1.98.1, edition 2024, on aarch64-apple-darwin. The quotes are excerpts.
Route the symptom to a section
| Symptom or task | Section |
error[E0277]: X cannot be sent between threads safely with help: the trait Sync is not implemented | The one rule |
error[E0277]: X cannot be shared between threads safely | The auto trait table |
error[E0321]: cross-crate traits with a default impl, like Send | E0321 |
A Mutex was changed to an RwLock and the build broke | Lock payload bounds |
MutexGuard<'_, T> cannot be sent between threads safely | Guards |
| You want a type that moves between threads but is not shareable | PhantomData surgery |
| An unrelated call site broke after you edited a function body | Auto traits leak |
error: future cannot be sent between threads safely | Auto traits leak |
warning: use of `async fn` in public traits is discouraged | the rust-async-internals skill, when it is installed |
A public type must keep or lose Send or Sync on purpose | Verify |
You are about to write unsafe impl Send for MyType {} | the rust-unsafe skill, when it is installed |
The one rule: &T is Send exactly when T is Sync
Sync states that one fact at the type level. A Send error whose help: line names Sync
therefore does not ask for a Send impl. It reports a shared reference to a value that is not
shareable.
Both auto traits are derived field-wise. Fix a bound error in the fields, not with an impl. Two helper functions turn any question into a compile error you can read:
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn main() {
assert_send::<&i32>(); // i32 is Sync, so &i32 is Send
assert_sync::<&i32>(); // &T is Sync exactly when T is Sync
assert_send::<std::cell::Cell<i32>>(); // Cell is Send: a single owner moves it
}Cell<i32> is Send. A shared reference to it is not:
use std::cell::Cell;
fn assert_send<T: Send>() {}
fn main() { assert_send::<&Cell<i32>>(); }error[E0277]: `&Cell<i32>` cannot be sent between threads safely
3 | fn main() { assert_send::<&Cell<i32>>(); }
| ^^^^^^^^^^ `&Cell<i32>` cannot be sent between threads safely
|
= help: the trait `Sync` is not implemented for `Cell<i32>`
= note: required for `&Cell<i32>` to implement `Send`Read the two lines together. The head trait is Send, because that is the bound that failed. The
unmet obligation is Sync, on the pointee. The failing impl is impl<T: Sync + ?Sized> Send for
&T, and you cannot write a different one (see E0321).
Repair in this order. Delete the sharing and move the value instead of the reference. Or give the
pointee interior mutability that is Sync: an atomic, a Mutex, or an RwLock. Only then read
the rust-unsafe skill, and only for a type you own whose own invariant makes the impl sound.
Never add unsafe impl Send or unsafe impl Sync only to make an error in this skill go away,
including on a wrapper struct around an Rc, a Cell, or a guard. The compiler accepts the impl
whether or not it is sound. Fix the field, the lock, or the signature instead.
The two message texts are not interchangeable. cannot be sent is a failed Send bound;
cannot be shared is a failed Sync bound. The second appears under an Arc, because
Arc<T>: Send itself requires T: Sync.
Verify the auto traits
The compiler proves only the auto traits it derives from fields. After a manual unsafe impl, it
no longer checks what a raw pointer field points to, so a green build proves nothing about that
impl.
| Claim | Check |
A public type stays Send or Sync | A const assertion next to the type (below). A field edit then fails at the definition, not at a distant caller. Across releases, cargo semver-checks reports auto_trait_impl_removed |
A public -> impl Trait return stays Send | Write + Send (and + Sync when callers share it) in the signature. The body then fails at the definition. See Auto traits leak |
A type is deliberately not Send | A compile_fail doctest that defines assert_send and calls assert_send::<my_crate::Type>(). A doctest is a separate crate, so crate::Type fails for the wrong reason. The doctest passes on any error, so keep only that call in it. Stable rustdoc does not check an error code after compile_fail |
An Arc payload is shareable | clippy::arc_with_non_send_sync, warn by default |
No std::sync guard lives across an .await | clippy::await_holding_lock, warn by default |
Every async fn future is Send | clippy::future_not_send (nursery, allow by default). Enable it in a library whose futures cross spawn. It reports at the definition |
cargo clippy --locked --all-targets -- -D warnings runs both warn-by-default lints above (group
suspicious). Run it before a commit that touches a type or a future that crosses threads. To
enable future_not_send, set future_not_send = "warn" under [lints.clippy] in Cargo.toml.
The lint reports every future-returning function in the crate, private functions and trait-impl
async fn included. Put #[expect(clippy::future_not_send, reason = "...")] on each future that
is local by design. The lint ignores a future that is Send only for some type parameters, so it proves nothing about a
generic async fn.
pub struct Session { pub id: u32, pub buf: Vec<u8> }
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Session>();
};The auto trait table
| Type | Send when | Sync when |
T (your struct) | every field is Send | every field is Sync |
&T | T: Sync | T: Sync |
&mut T | T: Send | T: Sync |
Box<T> | T: Send | T: Sync |
Arc<T> | T: Send + Sync | T: Send + Sync |
Rc<T> | never | never |
*const T, *mut T | never | never |
Cell<T>, RefCell<T> | T: Send | never |
Mutex<T>, RwLock<T> | T: Send | see lock bounds |
MutexGuard<'_, T>, RwLockReadGuard, RwLockWriteGuard | never | T: Sync |
Two rows carry the surprise. Arc<T> needs both traits on the payload, not only Send. An
Arc clone hands a second owner shared access to one value, so the payload must be shareable as
well as movable:
use std::cell::Cell;
use std::sync::Arc;
fn assert_send<T: Send>() {}
fn main() {
assert_send::<Cell<i32>>(); // ok on its own
assert_send::<Arc<Cell<i32>>>(); // E0277: Arc<T>: Send needs T: Send + Sync
}error[E0277]: `Cell<i32>` cannot be shared between threads safely
= help: the trait `Sync` is not implemented for `Cell<i32>`
= note: if you want to do aliasing and mutation between multiple threads, use
`std::sync::RwLock` or `std::sync::atomic::AtomicI32` instead
= note: required for `Arc<Cell<i32>>` to implement `Send`Put the lock inside the Arc. Arc<Mutex<Cell<i32>>> is
both Send and Sync, because Mutex<T>: Sync needs only T: Send. The note: means
"replace the Cell": Arc<AtomicI32> and Arc<RwLock<i32>> both work. Do not wrap the Cell
in the suggested RwLock: RwLock<Cell<i32>> fails again (see
lock bounds).
clippy::arc_with_non_send_sync (warn by default) reports Arc::new of a payload that is not
Send + Sync at the construction site, before any thread boundary. If that Arc never crosses a
thread, use Rc.
&mut T is not the mirror of &T. &mut T: Send requires T: Send, not T: Sync:
use std::cell::Cell;
use std::rc::Rc;
fn assert_send<T: Send>() {}
fn main() {
assert_send::<&mut Cell<i32>>(); // ok: Cell<i32> is Send, although it is not Sync
assert_send::<&mut Rc<i32>>(); // E0277: &mut T: Send needs T: Send
}error[E0277]: `Rc<i32>` cannot be sent between threads safely
= help: within `&mut Rc<i32>`, the trait `Send` is not implemented for `Rc<i32>`
= note: required because it appears within the type `&mut Rc<i32>`A &mut T is a move channel, not a view. std::mem::replace(x, T::default()) takes the value out
through the reference, so the receiving thread owns and drops it. That is the Send obligation.
unsafe impl Send cannot target a reference (E0321)
You cannot decide per reference type whether it is Send. The impl does not exist to be written:
pub struct Handle(*mut u8);
unsafe impl Send for &Handle {}
unsafe impl Send for &mut Handle {}error[E0321]: cross-crate traits with a default impl, like `Send`, can only be implemented
for a struct/enum type, not `&Handle`
2 | unsafe impl Send for &Handle {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ can't implement cross-crate trait with a default impl
| for non-struct/enum typeunsafe impl Send for Handle {} compiles, because Handle is a struct in this crate. Sync is
the only lever that reaches &Handle: write unsafe impl Sync for Handle {}, and &Handle
becomes Send through the blanket impl. That is a stronger promise. The rust-unsafe skill holds
the audit it needs.
Lock payload bounds: Mutex and RwLock are not interchangeable
Mutex<T>: Syncrequires onlyT: Send.RwLock<T>: SyncrequiresT: Send + Sync.
A Mutex hands out one &mut T at a time, so the payload never has to be shareable. An RwLock
hands out many concurrent &T, so the payload must be Sync on its own.
use std::cell::Cell;
use std::sync::Mutex;
fn assert_sync<T: Sync>() {}
fn main() { assert_sync::<Mutex<Cell<i32>>>(); } // compilesuse std::cell::Cell;
use std::sync::RwLock;
fn assert_sync<T: Sync>() {}
fn main() { assert_sync::<RwLock<Cell<i32>>>(); }error[E0277]: `Cell<i32>` cannot be shared between threads safely
= help: the trait `Sync` is not implemented for `Cell<i32>`
= note: required for `std::sync::RwLock<Cell<i32>>` to implement `Sync`Changing a read-heavy Mutex<T> to an RwLock<T> is therefore not a drop-in change. It stops
compiling as soon as the payload holds a Cell or a RefCell, and the error lands on the
Arc<RwLock<...>> at a call site far from the edit. Check the payload first. An Rc payload is
not a symptom of the swap. Mutex<T>: Sync also requires T: Send, so Mutex<Rc<T>> is neither
Send nor Sync before the edit. Mutex<T>: Send and RwLock<T>: Send both need only
T: Send, so the Send direction gives no warning.
Guards: MutexGuard is not Send, and it is Sync
std::sync::MutexGuard<'_, T> is !Send for every T. POSIX requires the unlocking thread to be
the locking thread, so the guard's Drop must run where the guard was created.
use std::sync::MutexGuard;
fn assert_send<T: Send>() {}
fn main() { assert_send::<MutexGuard<'static, i32>>(); }error[E0277]: `std::sync::MutexGuard<'static, i32>` cannot be sent between threads safely
= help: the trait `Send` is not implemented for `std::sync::MutexGuard<'static, i32>`Do not read that as "nothing derived from the guard leaves the thread". The guard is Sync
whenever T: Sync, so &MutexGuard<'_, T> is Send. Reading through the guard is reading
&T, and that is safe to share. This runs and prints total=6:
use std::sync::Mutex;
use std::thread;
fn main() {
let lock = Mutex::new(vec![1u32, 2, 3]);
let guard = lock.lock().expect("lock poisoned");
let total: u32 = thread::scope(|s| {
// &MutexGuard is Send, because MutexGuard is Sync.
s.spawn(|| guard.iter().sum::<u32>()).join().unwrap()
});
assert_eq!(total, 6);
println!("total={total}");
drop(guard); // the unlock runs on the locking thread
}Consequences to hold:
- Give a worker
&guardonly when the payload isSync.&MutexGuard<'_, T>isSendexactly whenT: Sync, so&MutexGuard<'_, Cell<T>>and&MutexGuard<'_, RefCell<T>>do not cross. Clone the value out of the guard instead. Never give a worker the guard itself. - Pass
&guardthroughthread::scope.thread::spawnrequires'static, so it rejects the borrow witherror[E0597]: `lock` does not live long enough, pluserror[E0373]when the closure borrowsguarddirectly. Do not apply the E0373movesuggestion: it moves the guard itself, and that fails with E0277. - Do not hold a
std::syncguard across an.await. The task can resume on another thread, so the future stops beingSend.clippy::await_holding_lock(warn by default) catches it. Therust-async-internalsskill covers the async lock choice.
PhantomData surgery
PhantomData<X> inherits the auto traits of X. Use PhantomData<Cell<()>> to remove only
Sync. Use a raw pointer marker such as PhantomData<*mut ()> only when you mean to remove both,
because it also blocks a move to a worker thread. Write () in the marker, not your own T: a
marker that names T also changes variance. Read
references/phantomdata-markers.md when you design a marker:
it has the auto-trait marker table, and the marker that removes only Send.
Auto traits leak out of impl Trait and async fn
A public -> impl Trait with no explicit + Send publishes whatever auto traits the body
happens to have. Adding one Rc inside the function is then a breaking change, and the compiler
reports it at every distant call site, with a note: that points into your private body:
error[E0277]: `Rc<Vec<u32>>` cannot be sent between threads safely
10 | std::thread::spawn(move || { let _: Vec<u32> = it.collect(); });
| ^^^^^^^ within this `{closure@...}`
note: required because it appears within the type `impl Iterator<Item = u32>`Spell the bound in the signature. The guarantee is then frozen, and the body is the thing that must comply:
pub fn ids() -> impl Iterator<Item = u32> + Send {
(0..3).map(|i| i * 2)
}
fn main() { assert_eq!(ids().sum::<u32>(), 6); }With + Send written, the same offending body fails at the definition:
use std::rc::Rc;
pub fn ids() -> impl Iterator<Item = u32> + Send {
let names: Rc<Vec<u32>> = Rc::new(vec![1, 2, 3]);
(0..3).map(move |i| names[i])
}error[E0277]: `Rc<Vec<u32>>` cannot be sent between threads safely
3 | pub fn ids() -> impl Iterator<Item = u32> + Send {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rc<Vec<u32>>` cannot be sent ...
5 | (0..3).map(move |i| names[i])
| ----------------------------- return type was inferred to be `Map<...>` hereWrite + Send only when you intend it, and add + Sync when callers share the returned value. An
API that is single-threaded by design cannot carry the bound, because it then fails at the
definition. Record that decision in the doc comment.
This is not async-specific. async fn f() -> T desugars to fn f() -> impl Future<Output = T>.
It has the same leak. On stable Rust 1.98.1, no syntax puts a bound on an async fn return type.
Write the desugared form:
use std::future::Future;
pub fn load(id: u32) -> impl Future<Output = u32> + Send {
async move { id + 1 }
}
fn main() { let _ = load(1); }Do not silence the async_fn_in_trait warning when callers spawn the future. For async fn
in a trait that callers spawn, and for return type notation, use the rust-async-internals
skill, when it is installed.
The async diagnostic has no error code, so a search for E0277 finds nothing:
error: future cannot be sent between threads safely
| ^^^^ future created by async block is not `Send`
note: future is not `Send` as this value is used across an await
5 | let names = Rc::new(vec![1u32, 2, 3]);
| ----- has type `Rc<Vec<u32>>` which is not `Send`
6 | std::future::ready(()).await;
| ^^^^^ await occurs here, with `names` maybe used laterRead the note:. It names the value and the exact .await that traps it. The
rust-compiler-errors skill covers the message shape; the fix here is the signature.
Related skills
Use these skills by name when they are installed.
| Skill | Boundary |
rust-unsafe | The proof obligation of a manual unsafe impl Send or Sync: the field audit, the compile-time field assertions, and the SAFETY comment |
rust-variance | The other half of a PhantomData marker: which substitutions the marker still allows, and the invariance a Cell or *mut marker introduces |
rust-compiler-errors | Reading E0277 in general, and the async block message that carries no error code |
rust-async-internals | Holding a guard across an .await, the async lock choice, Send trait variants, and cancel safety |
memory-model | Atomics, orderings, loom, and shared statics once the bounds are satisfied |
rust-crate-release | Semver review of a public auto trait change |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

