Advanced Traits
Send and Sync
std::marker::Send and std::marker::Sync are unsafe marker auto-traits that form the compile-time foundation of Rust's fearless concurrency:
T: Send: Ownership of a value of typeTcan be safely transferred (moved) to another thread.T: Sync: A value of typeTcan be safely accessed concurrently by multiple threads through shared references (&T).
The Fundamental Equivalence Connecting Send and Sync
A type T is Sync if and only if a shared reference &T is Send:
$$T: \text{Sync} \iff &T: \text{Send}$$
Similarly, sending an exclusive mutable reference &mut T to another thread requires that the target thread can safely own/mutate T:
$$&\text{mut } T: \text{Send} \iff T: \text{Send}$$
Moving `T` Across Threads (`std::thread::spawn(move || drop(val))`):
Thread 1 ---[ moves `val: T` ]---> Thread 2
Requires: `T: Send`
Sharing `&T` Across Threads (`std::thread::scope(|s| s.spawn(|| read(&val)))`):
Thread 1 (`val: T`) ---[ sends `&T` ]---> Thread 2 (`&T`)
Requires: `&T: Send`, which is identical to `T: Sync`!
Classification of Types by Send and Sync
| Category | Send? |
Sync? |
Examples & Explanation |
|---|---|---|---|
| Thread-safe values | Yes | Yes | Primitives (i32, bool), String, Vec<T>, Box<T>, Arc<T>, Mutex<T>, AtomicU64 |
| Movable single-thread cells | Yes | No | Cell<T>, RefCell<T>, OnceCell<T> — safe to move to another thread when uniquely owned, but unsafe to share via &T because their internal mutation is non-atomic |
| Thread-pinned OS guards | No | Yes | MutexGuard<'_, T> (where T: Sync) — can be read by scoped child threads via &MutexGuard, but cannot be moved to another thread because OS mutexes must be unlocked by the thread that locked them |
| Thread-local handles & raw pointers | No | No | Rc<T>, rc::Weak<T>, *const T, *mut T — neither safe to move nor share across threads |
Implementing Send and Sync Manually (unsafe impl)
When a custom struct encapsulates a raw pointer (*mut T or NonNull<T>), the auto-trait mechanism conservatively marks the struct !Send and !Sync. After auditing that your abstraction upholds thread-safety invariants, you opt back in using unsafe impl:
use std::ptr::NonNull;
use std::thread;
struct ReadOnlyBuffer {
ptr: NonNull<u8>,
len: usize,
}
// SAFETY: `ReadOnlyBuffer` points to immutable heap bytes that are never mutated
// after construction and are freed only when `ReadOnlyBuffer` is dropped by its sole owner.
unsafe impl Send for ReadOnlyBuffer {}
unsafe impl Sync for ReadOnlyBuffer {}
impl ReadOnlyBuffer {
fn from_vec(vec: Vec<u8>) -> Self {
let boxed: Box<[u8]> = vec.into_boxed_slice();
let len = boxed.len();
let raw: *mut u8 = Box::into_raw(boxed).cast::<u8>();
Self {
ptr: NonNull::new(raw).unwrap(),
len,
}
}
fn as_slice(&self) -> &[u8] {
// SAFETY: `self.ptr` is valid for `self.len` initialized bytes for `&self`.
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
}
}
impl Drop for ReadOnlyBuffer {
fn drop(&mut self) {
// SAFETY: Reconstruct the `Box<[u8]>` created in `from_vec` to free it.
unsafe {
let slice_ptr = std::ptr::slice_from_raw_parts_mut(self.ptr.as_ptr(), self.len);
drop(Box::from_raw(slice_ptr));
}
}
}
fn main() {
let buf = ReadOnlyBuffer::from_vec(vec![10, 20, 30, 40]);
// Share `&ReadOnlyBuffer` across scoped threads (uses `Sync`):
thread::scope(|s| {
s.spawn(|| println!("shared read in thread 1: {:?}", buf.as_slice()));
s.spawn(|| println!("shared read in thread 2: {:?}", buf.as_slice()));
});
// Move `ReadOnlyBuffer` into a spawned thread (uses `Send`):
thread::spawn(move || {
println!("owned read in thread 3: {:?}", buf.as_slice());
})
.join()
.unwrap();
}
shared read in thread 1: [10, 20, 30, 40]
shared read in thread 2: [10, 20, 30, 40]
owned read in thread 3: [10, 20, 30, 40]