Concurrency Fundamentals

The Sync Trait

std::marker::Sync is an unsafe auto-trait (pub unsafe auto trait Sync {}) indicating that it is safe for multiple threads to hold shared references (&T) to a value of type T at the same time. By definition in the standard library:

unsafe impl<T: Sync + ?Sized> Send for &T {}

A type T implements Sync if and only if its shared reference &T implements Send.

How Sync Enables Concurrent Shared Borrowing and static Globals

Sync is checked by the compiler in three major places:

  1. Sharing &T across scoped threads: Passing &val into std::thread::scope requires &T: Send, which requires T: Sync.
  2. Sharing via Arc<T>: Arc<T> only implements Send and Sync when T: Send + Sync, because every thread holding a cloned Arc<T> can dereference it to &T.
  3. Global static items: Because any thread can take a &'static T reference to a global static ITEM: T, the compiler requires T: Sync for every immutable static declaration (E0277).
// ❌ does not compile
use std::cell::RefCell;

// Error E0277: `static` items are shared across all threads, so their type
// MUST implement `Sync`. `RefCell<i32>` is `!Sync`!
static GLOBAL_CACHE: RefCell<i32> = RefCell::new(0);

fn main() {}
error[E0277]: `RefCell<i32>` cannot be shared between threads safely
 --> src/main.rs:6:22
  |
6 | static GLOBAL_CACHE: RefCell<i32> = RefCell::new(0);
  |                      ^^^^^^^^^^^^ `RefCell<i32>` cannot be shared between threads safely
  |
  = help: within `RefCell<i32>`, the trait `Sync` is not implemented for `UnsafeCell<i32>`
  = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicI32` instead
  = note: shared static variables must have a type that implements `Sync`

Why UnsafeCell<T> Is the Root of !Sync

In the standard library, UnsafeCell<T> (the foundational building block for all interior mutability) has an explicit negative implementation impl<T: ?Sized> !Sync for UnsafeCell<T> {}.

  • Any single-threaded wrapper around UnsafeCell<T> (Cell<T>, RefCell<T>, OnceCell<T>) inherits !Sync automatically because unsynchronized writes through &self from two threads would cause a data race.
  • Any thread-safe wrapper around UnsafeCell<T> (AtomicU64, Mutex<T>, RwLock<T>, OnceLock<T>) provides an explicit unsafe impl Sync because it guards the internal UnsafeCell<T> with hardware atomics or OS synchronization locks!
Why `Mutex<T>: Sync` Only Requires `T: Send` (Not `T: Sync`):

  Thread 1 (&Mutex<Cell<i32>>)        Thread 2 (&Mutex<Cell<i32>>)
              \                                  /
               v                                v
          +------------------------------------------+
          | Mutex<Cell<i32>>                         |
          |   Only ONE thread can hold MutexGuard    |
          |   at any time -> `Cell<i32>` is never    |
          |   accessed by two threads simultaneously!|
          +------------------------------------------+
use std::sync::Mutex;
use std::thread;

// `Mutex<Vec<u32>>` implements `Sync`, so it can live directly in a `static`:
static COMPLETED_JOBS: Mutex<Vec<u32>> = Mutex::new(Vec::new());

fn main() {
    thread::scope(|s| {
        for job_id in [101, 102, 103] {
            s.spawn(move || {
                COMPLETED_JOBS.lock().unwrap().push(job_id);
            });
        }
    });

    let mut jobs = COMPLETED_JOBS.lock().unwrap().clone();
    jobs.sort();
    println!("completed jobs: {:?}", jobs);
}
completed jobs: [101, 102, 103]