Smart Pointers

Smart Pointers and Thread Safety

Thread safety for smart pointers is governed at compile time by two auto-traits in std::marker: Send (a value can be transferred across thread boundaries) and Sync (a shared reference &T can be accessed concurrently from multiple threads; equivalently, T is Sync if and only if &T is Send).

Send and Sync Matrix for Smart Pointers and Containers

Type Send Implementation Rule Sync Implementation Rule Reason
Box<T> Send if T: Send Sync if T: Sync Unique owner; behaves identically to owning T directly
Rc<T>, rc::Weak<T> Never Send Never Sync Uses non-atomic Cell<usize> reference counters
Arc<T>, sync::Weak<T> Send if T: Send + Sync Sync if T: Send + Sync Shared &T across threads requires T: Sync; final drop on any thread requires T: Send
Cell<T> Send if T: Send Never Sync Moving a Cell<T> to another thread is safe, but concurrent .set() via &Cell<T> would race
RefCell<T> Send if T: Send Never Sync Runtime borrow counter is non-atomic
Mutex<T> Send if T: Send Sync if T: Send Serializes all access; even non-Sync T (like RefCell) is accessed by one thread at a time
RwLock<T> Send if T: Send Sync if T: Send + Sync Multiple threads can read &T simultaneously, requiring T: Sync
MutexGuard<'_, T> Never Send Sync if T: Sync OS mutexes (such as pthread_mutex_t) must be unlocked by the same thread that locked them
*const T, *mut T Never Send Never Sync Raw pointers carry no aliasing or synchronization guarantees

Compile-Time Prevention of Thread-Unsafe Sharing (E0277)

Attempting to send an Rc<T> or Arc<RefCell<T>> into std::thread::spawn produces a compile-time error (E0277) before the program can ever execute a data race:

// ❌ does not compile
use std::cell::RefCell;
use std::sync::Arc;
use std::thread;

fn main() {
    let shared_cell = Arc::new(RefCell::new(42));
    thread::spawn(move || {
        *shared_cell.borrow_mut() += 1;
    });
}
error[E0277]: `RefCell<i32>` cannot be shared between threads safely
 --> src/main.rs:8:19
  |
8 |     thread::spawn(move || {
  |     ------------- ^------
  |     |             |
  |     |             `RefCell<i32>` cannot be shared between threads safely
  |     required by a bound introduced by this call
  |
  = help: the trait `Sync` is not implemented for `RefCell<i32>`
  = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicI32` instead
  = note: required for `Arc<RefCell<i32>>` to implement `Send`

Why Arc<T> Requires Both T: Send and T: Sync to Be Send

Moving an Arc<T> clone to another thread (Arc<T>: Send) has two consequences:

  1. Both the original thread and the new thread now hold &T simultaneously $\implies$ requires T: Sync.
  2. Whichever thread drops the last Arc<T> will run T::drop and deallocate T on that thread $\implies$ requires T: Send.

Transferring a Non-Sync Type Safely with Arc<Mutex<T>>

Because Mutex<T> only requires T: Send to be both Send and Sync, wrapping a Send + !Sync type (such as Cell<T>, RefCell<T>, or an MPSC Sender<T> in older Rust versions) inside Mutex<T> allows Arc<Mutex<T>> to be shared across threads:

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

fn main() {
    // `Cell<u32>` is `Send` but `!Sync`.
    // Wrapping it in `Mutex` makes `Mutex<Cell<u32>>` both `Send` and `Sync`:
    let shared = Arc::new(Mutex::new(Cell::new(10_u32)));

    thread::scope(|s| {
        let clone = Arc::clone(&shared);
        s.spawn(move || {
            let guard = clone.lock().unwrap();
            guard.set(guard.get() + 25);
        });
    });

    println!("final value: {}", shared.lock().unwrap().get());
}
final value: 35