22.10 The Send and Sync Marker Traits

Two crucial marker traits underpin Rust’s compile-time concurrency safety: Send and Sync. They don’t define any methods; their purpose is to “mark” types with specific properties related to thread safety. The compiler automatically implements (or doesn’t implement) these traits for user-defined types based on their composition.

  • Send: A type T is Send if a value of type T can be safely transferred (moved) to another thread.

    • Most primitive types (i32, bool, f64, etc.) are Send.
    • Owned container types like String, Vec<T>, Box<T> are Send if their contained type T is also Send.
    • Arc<T> is Send if T is Send + Sync (shared ownership requires the inner type to be sharable too).
    • Mutex<T> and RwLock<T> are Send if T is Send.
    • Types that are not inherently Send:
      • Rc<T>: Its reference counting is non-atomic, making it unsafe to transfer ownership across threads where counts could be updated concurrently.
      • Raw pointers (*const T, *mut T): They don’t have safety guarantees, so they are not Send by default. Types containing raw pointers need careful consideration, often requiring unsafe impl Send.
  • Sync: A type T is Sync if a reference &T can be safely shared across multiple threads concurrently.

    • Technically, T is Sync if and only if &T (an immutable reference to T) is Send.
    • Most primitive types are Sync.
    • Immutable types composed of Sync types are typically Sync.
    • Arc<T> is Sync if T is Send + Sync.
    • Mutex<T> is Sync if T is Send. Even though the Mutex allows mutation of T, it synchronizes access, making it safe to share &Mutex<T> across threads. Access to the inner T is controlled via the lock.
    • RwLock<T> is Sync if T is Send + Sync (for readers) and T is Send (for writers).
    • Types that are not inherently Sync:
      • Cell<T>, RefCell<T>: These provide interior mutability without thread synchronization, making it unsafe to share &Cell<T> or &RefCell<T> across threads.
      • Rc<T>: Non-atomic reference counting makes sharing &Rc<T> unsafe.
      • Raw pointers (*const T, *mut T): Not Sync by default.

The compiler uses these traits implicitly when checking thread-related operations:

  • The closure passed to std::thread::spawn must be Send because it might be moved to a new thread. Any captured variables must also be Send.
  • Data shared using Arc<T> requires T: Send + Sync because multiple threads might access it concurrently via immutable references derived from the Arc.
  • Attempting to use a non-Send type across threads (e.g., putting an Rc<T> inside an Arc and sending it to another thread) will result in a compile-time error.
  • Attempting to share a non-Sync type (e.g., Arc<RefCell<T>>) across threads where multiple threads could potentially access it concurrently will also result in a compile-time error.

Understanding Send and Sync helps clarify why the Rust compiler allows certain concurrent patterns while forbidding others, forming the foundation of its “fearless concurrency” guarantee against data races in safe code.