Skip to content

Send and Sync Traits

Marker traits that encode thread-safety guarantees at the type level. The compiler uses them to prevent data races at compile time - Rust's key concurrency safety mechanism.

Key Facts

  • Send: type can be transferred to another thread (ownership move across thread boundary)
  • Sync: type can be shared between threads via shared reference (&T is Send iff T is Sync)
  • Both are auto-traits: compiler implements them automatically when all fields are Send/Sync
  • Both are marker traits: no methods, purely compile-time information
  • Almost all types are Send + Sync. Notable exceptions: Rc<T>, RefCell<T>, raw pointers
  • Manually implementing Send/Sync is unsafe - you assert the invariants hold

Rules

T is... Meaning
Send Safe to move T to another thread
!Send Must stay on creating thread
Sync Safe for multiple threads to hold &T simultaneously
!Sync Only one thread can reference at a time

Derived rule: T: Sync iff &T: Send

Common Types

Type Send Sync Why
i32, String, Vec<T> Yes Yes No shared mutable state
Rc<T> No No Non-atomic reference count
Arc<T> Yes Yes Atomic reference count
RefCell<T> Yes No Runtime borrow checking not thread-safe
Mutex<T> Yes Yes Lock serializes access
Cell<T> Yes No Non-atomic interior mutation
*const T, *mut T No No Raw pointers: no guarantees
MutexGuard<T> No Yes Must unlock on same thread

Patterns

Thread Spawn Requires Send

use std::thread;

let data = vec![1, 2, 3];
// Vec is Send, so this works:
thread::spawn(move || {
    println!("{:?}", data);
});

// Rc is !Send, so this fails:
// use std::rc::Rc;
// let rc = Rc::new(42);
// thread::spawn(move || {
//     println!("{}", rc);  // ERROR: Rc<i32> cannot be sent between threads
// });

Shared State Requires Sync

use std::sync::Arc;
use std::thread;

// Arc<T> is Send + Sync when T: Send + Sync
let shared = Arc::new(vec![1, 2, 3]);

let handles: Vec<_> = (0..3).map(|_| {
    let data = Arc::clone(&shared);
    thread::spawn(move || {
        println!("{:?}", data);  // &Vec is Send because Vec is Sync
    })
}).collect();

Move Closure and Send

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));

let handles: Vec<_> = (0..10).map(|_| {
    let counter = Arc::clone(&counter);
    thread::spawn(move || {
        // Mutex makes interior mutation Sync
        let mut num = counter.lock().unwrap();
        *num += 1;
    })
}).collect();

for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap()); // 10

Unsafe Send/Sync Implementation

struct MyWrapper(*mut u8);

// UNSAFE: you guarantee thread-safety invariants
unsafe impl Send for MyWrapper {}
unsafe impl Sync for MyWrapper {}

Negative Implementations

// How Rc opts out of Send/Sync (in std):
// impl<T> !Send for Rc<T> {}
// impl<T> !Sync for Rc<T> {}

// You can't write negative impls in stable Rust.
// Use PhantomData to opt out:
use std::marker::PhantomData;
use std::cell::Cell;

struct NotSync {
    _marker: PhantomData<Cell<()>>,  // Cell is !Sync
}

Decision Table

Need Use
Shared read-only data across threads Arc<T> where T: Sync
Shared mutable data across threads Arc<Mutex<T>> or Arc<RwLock<T>>
Single-thread shared ownership Rc<T>
Single-thread interior mutability RefCell<T> or Cell<T>
Transfer ownership to thread Anything Send via move closure

Gotchas

  • Issue: Rc<T> inside thread::spawn closure fails to compile -> Fix: Replace Rc with Arc. Arc is the thread-safe version with atomic reference counting.
  • Issue: RefCell<T> shared via Arc fails (RefCell is !Sync) -> Fix: Use Arc<Mutex<T>> or Arc<RwLock<T>> instead. Mutex provides thread-safe interior mutability.
  • Issue: MutexGuard held across .await point in async code -> Fix: MutexGuard is !Send, which blocks async tasks from moving between threads. Drop the guard before .await, or use tokio::sync::Mutex.

See Also