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:
- Sharing
&Tacross scoped threads: Passing&valintostd::thread::scoperequires&T: Send, which requiresT: Sync. - Sharing via
Arc<T>:Arc<T>only implementsSendandSyncwhenT: Send + Sync, because every thread holding a clonedArc<T>can dereference it to&T. - Global
staticitems: Because any thread can take a&'static Treference to a globalstatic ITEM: T, the compiler requiresT: Syncfor every immutablestaticdeclaration (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!Syncautomatically because unsynchronized writes through&selffrom two threads would cause a data race. - Any thread-safe wrapper around
UnsafeCell<T>(AtomicU64,Mutex<T>,RwLock<T>,OnceLock<T>) provides an explicitunsafe impl Syncbecause it guards the internalUnsafeCell<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]