Every thread and async-task API is defined in terms of Send and Sync; understanding them explains a whole class of compiler errors.
Thread safety in Rust is two marker traits with no methods:
- `Send`: a value of this type can be moved to another thread.
- `Sync`: a value can be shared between threads by reference. Formally,
T: Syncexactly when&T: Send.
They are auto traits: the compiler implements them for your type automatically when all of its fields are Send/Sync. That is why you rarely think about them until a type that lacks them sneaks into a struct.
Instructive exceptions in std:
Rc<T>is neither: two threads cloning it would race on the non-atomic count.Arc<T>isSend + SyncwhenT: Send + Sync.Cell<T>andRefCell<T>areSend(ifT: Send) but notSync: one thread may own and mutate them, but shared access from two threads would race.Mutex<T>isSyncwhenT: Send. That is the whole trick: the lock turns aSendtype into one that can be shared.MutexGuardis notSend, because some platforms require unlocking on the locking thread. In async code this is why a std guard held across.awaitmakes the future non-Sendandtokio::spawnrejects it.- Raw pointers are neither, as a conservative default so that types built on them must opt in deliberately.
When you build a type on raw pointers that is in fact thread-safe, you implement the traits yourself: unsafe impl Send for MyBuf {}. The unsafe is the point: you are promising the compiler that every API on the type upholds thread safety, and getting it wrong means data races.
To opt out on stable Rust, add a field such as PhantomData<*const ()> (neither) or PhantomData<Cell<()>> (Send but not Sync). Compile-time checks like fn assert_send<T: Send>() {} are a cheap way to lock in the guarantees your API promises.
Example
use std::{cell::Cell, marker::PhantomData, sync::{Arc, Mutex}};
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
// Owns a heap buffer through a raw pointer, so it is not Send automatically.
struct Buffer { ptr: *mut u8, len: usize }
// SAFETY: Buffer uniquely owns its allocation; nothing else aliases `ptr`.
unsafe impl Send for Buffer {}
impl Drop for Buffer {
// SAFETY: ptr and len come from the Box<[u8]> created in main
fn drop(&mut self) { unsafe { drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(self.ptr, self.len))) } }
}
struct SingleThreaded { _not_sync: PhantomData<Cell<()>> } // Send, but opted out of Sync
fn main() {
assert_send::<Arc<Mutex<Vec<u8>>>>();
assert_sync::<Mutex<Cell<u8>>>(); // Mutex makes a Send-only type Sync
assert_send::<SingleThreaded>();
// assert_sync::<Cell<u8>>(); // error: Cell<u8> cannot be shared between threads
// assert_sync::<SingleThreaded>(); // error: opted out via PhantomData<Cell<()>>
let buf = Buffer { ptr: Box::into_raw(vec![0u8; 64].into_boxed_slice()).cast(), len: 64 };
let len = std::thread::spawn(move || { let b = buf; b.len }).join().unwrap(); // needs Buffer: Send
println!("{len}"); // 64 (the buffer is freed on the worker thread)
}Remember
Send= can move to another thread;Sync=&Tcan be shared across threads- Both are auto traits, derived from the fields
Rcis neither,Cell/RefCellare Send not Sync,Mutexmakes Send data Sync- Implementing them is
unsafe; opt out with aPhantomDatafield
Watch out for
- Writing
unsafe impl Syncjust to silence a compiler error without proving thread safety.