Introduction
Rust's fearless concurrency guarantee rests on two marker traits: Send and Sync. The Send trait marks types whose ownership can be safely transferred to another thread. Without Send, the compiler refuses to let you move a value into a thread::spawn closure — this single rule prevents an entire class of data races at compile time.
Key Concepts
- Send: A marker trait (no methods) that indicates a type is safe to move to another thread. If a type is
Send, you can transfer ownership of a value of that type from one thread to another. - Auto-trait:
Sendis automatically implemented for any type whose fields are allSend. You never need to writeimpl Send for MyStructunless you are wrapping a non-Sendraw pointer and can guarantee safety. - !Send types: Types that are explicitly not
Send. The most common areRc<T>(non-atomic reference count), raw pointers (*const T,*mut T), andMutexGuard<T>(must be dropped on the same thread that locked it). - thread::spawn bounds: The closure passed to
thread::spawnmust beSend + 'static, and its return type must also beSend + 'static. This is the compiler's enforcement point — if any captured variable is notSend, the call fails to compile.
Real World Context
Every time you call thread::spawn, the compiler checks that every value moved into the closure is Send. This prevents you from accidentally sharing an Rc across threads (which would corrupt its reference count) or sending a MutexGuard to another thread (which would violate the lock's invariants). In practice, most types you write are automatically Send because they are composed of Send fields. The compiler only stops you when you reach for something inherently thread-unsafe.
Deep Dive
Send is defined as a marker trait with no methods. The compiler auto-implements it for types composed entirely of Send fields.
rust// Send is defined roughly as: pub unsafe auto trait Send {}
Because Send is an auto-trait, your own structs get it for free when all their fields are Send.
rustuse std::sync::Arc; use std::rc::Rc; struct MySendType { name: String, // Send count: Arc<i32>, // Send values: Vec<u64>, // Send } // MySendType is automatically Send because all fields are Send. struct NotSend { data: Rc<String>, // Rc is !Send } // NotSend is NOT Send because Rc<String> is !Send.
The thread::spawn function enforces Send at compile time. Both the closure and its return type must be Send.
rustuse std::thread; // This compiles — String and usize are both Send. let s = String::from("hello"); let handle = thread::spawn(move || { println!("{s}"); s.len() // usize is Send }); let length: usize = handle.join().unwrap();
Attempting to send an Rc to another thread produces a compile error.
rustuse std::rc::Rc; use std::thread; let rc = Rc::new(42); // This fails to compile: // thread::spawn(move || { // println!("{}", *rc); // }); // Error: `Rc<i32>` cannot be sent between threads safely
The reason Rc is !Send is that its reference count uses non-atomic operations. If two threads could hold clones of the same Rc, they could increment or decrement the count simultaneously, causing a data race.
rustuse std::rc::Rc; let rc1 = Rc::new(5); let rc2 = Rc::clone(&rc1); // non-atomic refcount increment // If rc1 were sent to another thread: // Thread A: drop(rc1) — decrements count non-atomically // Thread B: drop(rc2) — decrements count non-atomically // Both decrements happen simultaneously — data race on the count! // The fix: use Arc, which uses atomic operations for the count.
Raw pointers are !Send by default because the compiler cannot verify that the pointed-to data is safe to access from another thread.
ruststruct Wrapper { ptr: *mut u8, } // Wrapper is !Send because *mut u8 is !Send. // If you can guarantee thread safety, you can opt in manually: unsafe impl Send for Wrapper {} // This is YOUR responsibility — the compiler trusts you.
You can verify whether a type is Send at compile time with a helper function.
rustfn assert_send<T: Send>() {} assert_send::<String>(); // compiles assert_send::<Vec<i32>>(); // compiles assert_send::<std::sync::Arc<String>>(); // compiles // assert_send::<Rc<i32>>(); // compile error: Rc is !Send // assert_send::<*mut u8>(); // compile error: raw pointers are !Send
Common Pitfalls
- Wrapping a non-Send type does not make it Send — If your struct contains an
Rcor a raw pointer, the entire struct becomes!Send. You must replace the non-Send field (e.g., useArcinstead ofRc) or useunsafe impl Sendwith a correctness proof. - Confusing Send with Copy or Clone —
Sendis about thread safety, not about duplication. AStringisSend(safe to move to another thread) but notCopy. AnRcisClonebut notSend.
Best Practices
- Let the compiler derive Send automatically — In almost all cases, your types will be
Sendbecause their fields areSend. Only reach forunsafe impl Sendwhen wrapping FFI pointers or other raw types where you can guarantee safety. - Use
Arcinstead ofRcfor shared ownership across threads —Arcuses atomic reference counting and isSend + Sync. It is the direct thread-safe replacement forRc.
Summary
Sendmeans a value can be safely moved to another thread.- It is an auto-trait: your type is
Sendif all its fields areSend. Rc, raw pointers, andMutexGuardare!Send.thread::spawnenforcesSend + 'staticon the closure and its return type.- Use
unsafe impl Sendonly when you can prove thread safety for a raw-pointer-containing type.
Code Examples
use std::thread;
use std::sync::Arc;
// Compile-time Send check helper
fn assert_send<T: Send>() {}
fn main() {
// These all compile — all are Send
assert_send::<String>();
assert_send::<Vec<i32>>();
assert_send::<Arc<String>>();
assert_send::<i32>();
// Rc is NOT Send — this would fail:
// assert_send::<std::rc::Rc<i32>>();
// thread::spawn enforces Send on the closure and return type
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
let sum: i32 = data.iter().sum();
println!("sum = {sum}");
sum // return type must also be Send
});
let result = handle.join().unwrap();
println!("thread returned: {result}");
}