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 type T can be safely transferred (moved) to another thread.
  • T: Sync: A value of type T can 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]