Send and Sync

Rust · Expert · 15 min

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: Sync exactly 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> is Send + Sync when T: Send + Sync.
  • Cell<T> and RefCell<T> are Send (if T: Send) but not Sync: one thread may own and mutate them, but shared access from two threads would race.
  • Mutex<T> is Sync when T: Send. That is the whole trick: the lock turns a Send type into one that can be shared.
  • MutexGuard is not Send, because some platforms require unlocking on the locking thread. In async code this is why a std guard held across .await makes the future non-Send and tokio::spawn rejects 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 = &T can be shared across threads
  • Both are auto traits, derived from the fields
  • Rc is neither, Cell/RefCell are Send not Sync, Mutex makes Send data Sync
  • Implementing them is unsafe; opt out with a PhantomData field

Watch out for

  • Writing unsafe impl Sync just to silence a compiler error without proving thread safety.

Check yourself

  1. Which statement is true?
  2. Why does `tokio::spawn` reject a future holding a `std::sync::MutexGuard` across `.await`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/nomicon/send-and-sync.html