TL;DR:
Sendmeans a type can be moved to another thread.Syncmeans a type can be shared by reference across threads (i.e.,&TisSend). Both are auto-traits, the compiler derives them automatically for most types. Types that are neither (Rc<T>, raw pointers) cannot cross thread boundaries. This is how Rust guarantees data-race freedom at compile time: if your code compiles, it's thread-safe.
What Is Send?
Send is a marker trait that says: it is safe to move a value of this type to a different thread.
use std::thread;
fn spawn_with<T: Send + 'static>(val: T) {
thread::spawn(move || {
// `val` was moved here; to a new OS thread
println!("running in a new thread");
drop(val);
});
}
spawn_with(String::from("hello")); // String: Send ✅
spawn_with(42u32); // u32: Send ✅
// spawn_with(Rc::new(1)); // Rc<T>: !Send ❌; compile errorMost types are automatically Send. The exceptions are types that rely on thread-local state or non-atomic reference counts, like Rc<T> and raw pointers.
What Is Sync?
Sync is a marker trait that says: it is safe to share a reference to this type across multiple threads simultaneously.
A type T is Sync if &T is Send, meaning a shared reference to T can be sent to another thread without causing data races.
use std::sync::Arc;
use std::thread;
let data = Arc::new(42u32); // u32: Sync, so Arc<u32>: Send + Sync
let data2 = Arc::clone(&data);
thread::spawn(move || {
println!("{}", *data2); // multiple threads read simultaneously; safe
});
println!("{}", *data); // main thread also reads; safeArc<T> is Sync when T: Send + Sync. Mutex<T> is Sync because it serializes access, even if T is not Sync on its own.
Which Common Types Are Not Send or Sync?
Rc<T> and Cell<T>/RefCell<T> are the main !Send/!Sync types you'll encounter. Raw pointers are also !Send + !Sync.
| Type | Send | Sync | Reason |
|---|---|---|---|
Rc<T> | ❌ | ❌ | Non-atomic ref count, races if shared |
RefCell<T> | ✅ | ❌ | Runtime borrow tracking is not thread-safe |
Cell<T> | ✅ | ❌ | Interior mutability without synchronization |
*mut T / *const T | ❌ | ❌ | Raw pointers, no safety guarantees |
Arc<T> | ✅ (if T: Send+Sync) | ✅ | Atomic ref count |
Mutex<T> | ✅ (if T: Send) | ✅ | Serializes access |
MutexGuard<T> | ❌ | ✅ (if T: Sync) | Must be unlocked on same thread |
Why Does the Compiler Say "Future is not Send"?
The most common Send error in async code: a future holds a !Send type across an .await point. The fix is usually replacing Rc with Arc or avoiding holding non-Send guards across awaits.
use std::rc::Rc;
// This will NOT compile with tokio::spawn:
async fn broken() {
let x = Rc::new(42); // Rc is !Send
some_async_work().await; // x is held across .await
println!("{x}");
}
// Fix: use Arc instead
async fn fixed() {
let x = std::sync::Arc::new(42); // Arc is Send
some_async_work().await;
println!("{x}");
}Other common causes: holding a MutexGuard across .await (use tokio::sync::Mutex instead), or capturing non-Send closures.
How Do You Implement Send and Sync Manually?
You can implement Send/Sync manually with unsafe impl, only do this when you have verified thread-safety that the compiler can't infer.
use std::sync::Arc;
struct MyThreadSafeWrapper(*mut u8); // raw pointer; normally !Send
// SAFETY: we guarantee that access to the inner pointer
// is synchronized externally and the pointer is valid
unsafe impl Send for MyThreadSafeWrapper {}
unsafe impl Sync for MyThreadSafeWrapper {}This is how the standard library itself implements Send/Sync for types like Mutex<T> and Arc<T>. Only reach for unsafe impl when you have proven correctness, it bypasses the compiler's guarantees.
Frequently Asked Questions
The compiler automatically derives Send and Sync for any type where all fields are Send/Sync. This means you get thread safety for free for normal structs, you only need to think about it when your type contains raw pointers or non-atomic shared state.
Yes, if T: Send. Arc provides shared ownership via atomic ref counting (Sync), and Mutex serializes mutable access. Together they're the standard pattern for thread-safe shared mutable state.
'static means the type contains no non-'static references, the spawned task must own all its data (or use Arc). Send means it can be moved to a different worker thread in Tokio's thread pool.
Yes, but it's rare. MutexGuard<T> is Sync (if T: Sync) but not Send, a lock guard must be released on the same thread it was acquired on.
Sources
Related Glossary Terms
- Arc:
Arc<T>isSend + SyncwhenT: Send + Sync, the thread-safe ownership type - Mutex:
Mutex<T>makes a!SynctypeSyncby serializing access - Async/Await:
tokio::spawnrequires futures to beSend + 'static - Rc:
Rc<T>is!Send + !Sync, the single-threaded alternative toArc - Unsafe: Manual
Send/Syncimpls requireunsafe impl - RwLock:
RwLock<T>is one of the most common synchronization primitives whose safety depends onSend + Sync - once-cell:
OnceLockand once_cell matter because their global access patterns still rely onSend + Sync
Keep Reading
- Rust Ownership and Borrowing Explained: Send and Sync are the ownership traits that make threads safe
- Rust Memory Safety: NSA and CISA: compile-time thread safety guarantees
- Rust in the Linux Kernel: Send + Sync enforced in kernel driver abstractions
