Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6.8 · Send, Sync, and the Marker Trait Contracts

Domain 6 — Concurrency and Parallelism Duration: ~15 minutes Library components: std::marker::Send, std::marker::Sync, std::marker::Unpin, std::marker::PhantomData, std::marker::PhantomPinned

Introduction

Two marker traits with no methods are the base of all the topics in this domain. They explain the signature of thread::spawn, why Arc exists together with Rc, and why a Mutex makes data shareable. Send and Sync are the implementation of the "fearless concurrency" of Rust. Thread safety is a type property: the compiler derives it automatically and checks it at compile time.

This tutorial shows:

  • The contracts: what Send and Sync promise, and why they are unsafe auto traits.
  • What breaks Send (Rc<T>), what breaks Sync (Cell<T>, RefCell<T>), and the substitution that repairs each one.
  • How Arc<Mutex<T>> satisfies the two contracts, layer by layer.
  • How to opt out with PhantomData and opt in with unsafe impl, and the proof that each one requires.
  • How Unpin and PhantomPinned relate to the other marker traits.

Two Contracts, Zero Methods

  • Send: ownership of the value may move to another thread.
  • Sync: threads may share a &T at the same time.

One rule connects the two traits: T: Sync if and only if &T: Send. To share a value is to send references to it. The two traits are two views of one idea.

The two traits are auto traits: the compiler derives them from the structure of a type. A struct is Send when each field is Send, and Sync when each field is Sync. You write no annotation and no derive:

struct Inventory {          // Send + Sync automatically, because each field is:
    items: Vec<String>,     //   Vec<String> is Send + Sync
    total: usize,           //   usize is Send + Sync
}

The two traits are also unsafe traits: a manual implementation is a promise that the compiler cannot check. This divides the work. The compiler derives the traits from structure for 99% of the types. A person takes written responsibility for the other 1% (slide 6).

The examples use these compile-time probes:

// A call to a probe compiles only if the type `T` satisfies the bound.
// The bodies are empty, so the probes do no work at run time.
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}

assert_send::<String>();          // compiles: String is Send
// assert_send::<Rc<String>>();   // does not compile: Rc<String> is not Send
// error[E0277]: `Rc<String>` cannot be sent between threads safely

What Breaks Send: Rc<T>

The reference count of an Rc is a plain integer that is not atomic. That is the performance advantage of Rc. If two threads clone or drop the same allocation, they race on the count. The race can have these results:

  • The count loses increments.
  • The program frees the value while a thread still uses it.
  • The value leaks forever.

Thus Rc is neither Send nor Sync, and the compiler rejects the code at the call site:

let rc = Rc::new(String::from("local"));   // rc: Rc<String>
// The closure captures `rc` and moves it to the new thread.
// This line does not compile:
// thread::spawn(move || rc.len());
// error[E0277]: `Rc<String>` cannot be sent between threads safely
//   note: required because it's used within this closure
//   note: required by a bound in `spawn`

Read that error as design feedback, not as an obstacle: it names the exact type that you must replace. The substitution is Arc. It has an atomic count, the same API, and a small cost:

let arc = Arc::new(String::from("shared"));   // arc: Arc<String>
let for_worker = Arc::clone(&arc);            // a second handle to the same String
thread::spawn(move || for_worker.len());      // compiles and is correct: Arc<String> is Send

This is the effect of the bound in the signature of thread::spawn: F: Send + 'static. The closure moves to the new thread, so each value that it captures must be Send. One captured Rc makes the full closure not Send. Auto traits propagate through closures exactly as they propagate through structs.

What Breaks Sync: Cell and RefCell

Cell::set writes through &self with no synchronization. If two threads do that at the same time, the result is a data race by definition. Note the asymmetry: Cell<T: Send> is Send, because a move of the full cell to one other thread is harmless. A Cell only refuses to be shared:

let shared = Arc::new(Cell::new(0_u64));   // shared: Arc<Cell<u64>>
let clone = Arc::clone(&shared);           // a second handle to the same Cell
// The closure moves `clone` to a new thread, so two threads share one Cell.
// This line does not compile:
// thread::spawn(move || clone.set(1));
// error[E0277]: `Cell<u64>` cannot be shared between threads safely
//   note: required for `Arc<Cell<u64>>` to implement `Send`

Arc<T> is Send only when T: Send + Sync. The only job of an Arc is to share its payload, so it cannot hold a payload that is not shareable.

RefCell fails for the same reason. Its borrow counter is not atomic, so its runtime borrow checks (Tutorial 7.3) are sound only on one thread.

Figure: The Substitution Map

The compiler enforces each row of the figure. The types on the left side cannot cross a thread boundary. Thus the upgrade is never optional, and you cannot forget it.

How Arc<Mutex<T>> Satisfies Both

Examine this common combination from the inner layer to the outer layer:

  1. T: Send: the data can move between threads. For example, the thread that holds the last handle drops the data.
  2. Mutex<T>: Send + Sync when T: Send: the lock supplies the exclusion that T does not have. Even a RefCell becomes shareable in a Mutex. Only one thread can access it at a time, so no race on its non-atomic internals can occur.
  3. Arc<Mutex<T>>: Send + Sync: Arc requires a payload that is Send + Sync, and the Mutex layer guarantees that. Arc adds shared ownership with an atomic count.

Each layer adds exactly one capability: Mutex makes the data shareable, and Arc makes the data shared. The bounds are conditional, so the compiler checks the full stack for each instantiation. Arc<Mutex<Rc<u8>>> still fails, because Rc<u8> is not Send. A clone of the Arc on another thread could drop the Rc<u8> on that thread.

One more fact is useful. MutexGuard is deliberately !Send, because some OS mutexes require that the thread that locks them also unlocks them. But MutexGuard is Sync when T: Sync. Scoped threads can borrow a guard, but the thread that locked the mutex must drop the guard.

PhantomData to Opt Out, unsafe impl to Opt In

To opt out. A usize handle can represent a session of a C library. To the compiler, the handle looks Send + Sync, because its structure is only an integer. If the threading rules of the C library do not agree, a zero-sized PhantomData field encodes the policy:

struct FfiSession {
    raw_handle: usize,   // represents a C pointer or a C token
    // `*const ()` is neither Send nor Sync, so this field removes the two
    // auto traits from the struct. The field has a size of zero bytes.
    _not_thread_safe: PhantomData<*const ()>,
}
// assert_send::<FfiSession>();   // does not compile
// error[E0277]: `*const ()` cannot be sent between threads safely

Each downstream user now gets a compile error, not a runtime bug that is hard to reproduce. PhantomData<Rc<()>> also works. The raw-pointer form is the convention for an FFI-related type that is not thread-safe.

To opt in. Raw pointers are !Send and !Sync because of suspicion, not because of necessity: the compiler cannot know what they point to. When you can prove exclusive ownership, you may override the compiler:

// `ptr` is the only pointer to a heap allocation of `len` bytes.
// The raw pointer field makes the struct !Send and !Sync by default.
struct ExclusiveBuffer { ptr: *mut u8, len: usize }

// SAFETY: ExclusiveBuffer owns its allocation exclusively. No other pointer
// to the allocation exists, and the API never gives one to a caller. Thus a
// move of the struct to another thread moves the buffer as it moves any owned
// value. Nothing stays on the first thread that can race with the buffer.
unsafe impl Send for ExclusiveBuffer {}

The requirement for an unsafe impl is a written argument about aliasing and synchronization. "The tests pass" is not such an argument. If the struct ever gives a second pointer to other code, the impl becomes unsound, and the compiler never gives a warning.

Note that the example claims only Send, not Sync. Claim the minimum that you need. Prefer to compose std types that are already thread-safe. Write manual impls only at FFI boundaries.

06_27_phantom_unsafe_impl.rs prints:

FfiSession usable locally; !Send enforced at compile time
ExclusiveBuffer summed on another thread: 192

All assertions passed.

Unpin and PhantomPinned: The Other Marker Pair

Unpin is another auto marker trait, with a different contract. Send and Sync control where a value may go (threads). Unpin controls whether a value may move after you pin it (addresses). Almost all types are Unpin. For example, a move of a String or a Vec never invalidates it:

// A compile-time probe of the same form as `assert_send`.
fn assert_unpin<T: Unpin>() {}

assert_unpin::<String>();             // compiles: String is Unpin
// FfiSession is the !Send + !Sync struct from the previous section. It is Unpin.
assert_unpin::<Vec<FfiSession>>();    // compiles

Self-referential types (the state machines of an async fn, intrusive linked nodes) store pointers into themselves. A move of such a value corrupts those pointers. These types opt out with PhantomPinned, exactly as FfiSession opts out of Send and Sync with PhantomData<*const ()>:

// PhantomPinned is a zero-sized type that is not Unpin.
// Thus a struct that contains it is not Unpin.
struct SelfReferential { _pin: PhantomPinned }
// assert_unpin::<SelfReferential>();   // does not compile
// error[E0277]: `PhantomPinned` cannot be unpinned

For now, the important point is the symmetry: a zero-sized marker field changes an invariant into a compile-time property. Domain 15 continues with Pin and the async mechanisms that use it.

Summary

ConceptKey point
SendOwnership may move to another thread
SyncThreads may share a &T. T: Sync ⇔ &T: Send.
Auto traitsThe compiler derives them from structure. One field without the property removes it from the full type.
unsafe traitsA manual impl is a promise that a person checks, not the compiler
Rc breaks SendThe reference count is not atomic. Use Arc.
Cell/RefCell break SyncThe interior mutability is not atomic. Use Atomic*, Mutex, or RwLock.
Arc<Mutex<T>>Mutex makes the data shareable, and Arc makes the data shared. The combination needs only T: Send.
MutexGuard!Send (the thread that locks must also unlock), but Sync when T: Sync
PhantomData<*const ()>A zero-sized field that opts a handle type out of Send and Sync
unsafe impl Send/SyncRequires a proof about aliasing and synchronization. Claim the minimum.
Unpin / PhantomPinnedThe same marker mechanism controls whether a pinned value may move (Domain 15)

Code Examples

FileDescription
06_25_send_sync_auto.rsCompile-time probes, automatic propagation, the relation between &T and Sync, and Arc<Mutex<T>>
06_26_not_send_not_sync.rsThe failures of Rc, Cell, and RefCell with the exact errors, and the substitution table
06_27_phantom_unsafe_impl.rsAn opt-out with PhantomData, a justified unsafe impl Send, and PhantomPinned