Introduction
Sync is the companion to Send. While Send governs moving values between threads, Sync governs sharing references between threads. A type T is Sync if and only if &T is Send — meaning multiple threads can safely hold shared references to the same value simultaneously. This trait is the compile-time foundation for safe concurrent reads.
Key Concepts
- Sync: A marker trait indicating that shared references (
&T) to the type can be safely sent to other threads. IfT: Sync, multiple threads can read from&Tconcurrently. - The equivalence:
T: Syncif and only if&T: Send. This is not just a mental model — it is the formal definition. If you can safely send a reference to another thread, the type is Sync. - !Sync types: Types that allow mutation through shared references without synchronization:
Cell<T>,RefCell<T>,Rc<T>, and the primitiveUnsafeCell<T>. These use interior mutability without atomic operations or locks, making concurrent access unsafe. - Mutex makes things Sync:
Mutex<T>isSyncwheneverT: Send, even ifTitself is notSync. The mutex ensures only one thread accessesTat a time, providing the synchronization thatTlacks.
Real World Context
When you place data behind an Arc, the compiler checks that T: Sync (because Arc lets multiple threads hold &T through its Deref implementation). This is why Arc<RefCell<T>> does not compile — RefCell is !Sync because its runtime borrow checking is not thread-safe. Understanding Sync tells you exactly which types can live inside an Arc and which need a Mutex wrapper.
Deep Dive
The defining relationship is simple: T is Sync if a shared reference &T can be safely sent to another thread.
rustfn assert_sync<T: Sync>() {} assert_sync::<i32>(); // immutable reads are always safe assert_sync::<String>(); // String is Sync assert_sync::<Vec<u8>>(); // Vec is Sync assert_sync::<std::sync::Mutex<i32>>(); // Mutex is Sync // assert_sync::<std::cell::Cell<i32>>(); // compile error: Cell is !Sync // assert_sync::<std::cell::RefCell<i32>>(); // compile error: RefCell is !Sync // assert_sync::<std::rc::Rc<i32>>(); // compile error: Rc is !Sync
The following table shows the Send and Sync status of the most important standard library types, and the reasoning behind each.
| Type | Send | Sync | Reasoning |
|---|---|---|---|
i32, f64, bool | Yes | Yes | Primitive types with no interior state |
String, Vec<T> | Yes (if T: Send) | Yes (if T: Sync) | Owned heap data, no interior mutability |
Rc<T> | No | No | Non-atomic reference count |
Arc<T> | Yes (if T: Send + Sync) | Yes (if T: Send + Sync) | Atomic reference count |
Cell<T> | Yes (if T: Send) | No | Interior mutability without synchronization |
RefCell<T> | Yes (if T: Send) | No | Runtime borrow tracking is not atomic |
Mutex<T> | Yes (if T: Send) | Yes (if T: Send) | Lock provides synchronization |
RwLock<T> | Yes (if T: Send) | Yes (if T: Send + Sync) | Lock provides synchronization |
MutexGuard<'a, T> | No | Yes (if T: Sync) | Must be dropped on the locking thread |
UnsafeCell<T> | Yes (if T: Send) | No | The raw building block for interior mutability |
Cell and RefCell are !Sync because they allow mutation through &self (interior mutability) without any synchronization mechanism. If two threads held &Cell<i32> simultaneously, both could call .set() at the same time, causing a data race.
rustuse std::cell::Cell; use std::thread; let cell = Cell::new(0); // If Cell were Sync, we could do this: // thread::scope(|s| { // s.spawn(|| cell.set(1)); // mutation via &Cell // s.spawn(|| cell.set(2)); // concurrent mutation — data race! // }); // The compiler prevents this because Cell is !Sync.
Mutex<T> is Sync even when T is !Sync, because the mutex ensures only one thread can access the inner value at any time. The bound is T: Send, not T: Sync — the data must be transferable between threads (since any thread can acquire the lock), but it does not need to support concurrent access (the lock prevents that).
rustuse std::sync::{Arc, Mutex}; use std::cell::RefCell; use std::thread; // RefCell is !Sync, but Mutex<RefCell<T>> is Sync. let shared = Arc::new(Mutex::new(RefCell::new(vec![1, 2, 3]))); thread::scope(|s| { for i in 0..3 { let shared = Arc::clone(&shared); s.spawn(move || { let guard = shared.lock().unwrap(); let mut borrow = guard.borrow_mut(); borrow.push(i * 10); println!("thread {i}: {:?}", *borrow); }); } });
The equivalence T: Sync means &T: Send is visible in scoped threads. When you borrow local data in a scoped thread, the compiler checks that the reference type is Send, which is equivalent to the data type being Sync.
rustuse std::thread; let data = vec![1, 2, 3, 4, 5]; thread::scope(|s| { // &Vec<i32> is Send because Vec<i32> is Sync. // Multiple threads can safely hold &Vec<i32>. s.spawn(|| println!("thread 1: {:?}", &data)); s.spawn(|| println!("thread 2: {:?}", &data)); });
Arc<T> requires T: Send + Sync because Arc allows multiple threads to hold a shared reference to T. If T were !Sync, concurrent &T access would be unsafe.
rustuse std::sync::Arc; // Works: i32 is Send + Sync let a: Arc<i32> = Arc::new(42); // Works: Mutex<T> is Send + Sync when T: Send use std::sync::Mutex; let b: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![])); // Does NOT compile: RefCell is !Sync // let c: Arc<RefCell<i32>> = Arc::new(RefCell::new(0)); // Error: `RefCell<i32>` cannot be shared between threads safely
Common Pitfalls
- Assuming Arc makes anything thread-safe —
Arc<T>requiresT: Send + Sync. IfTis!Sync(likeRefCell), you must wrap it in aMutexfirst:Arc<Mutex<RefCell<T>>>, or better yet, just useArc<Mutex<T>>directly sinceMutexalready provides interior mutability. - Confusing Sync with immutability —
Syncdoes not mean immutable.Mutex<T>isSyncand provides mutable access.Syncmeans that whatever concurrent access the type permits is properly synchronized.
Best Practices
- Use the relationship table as a quick reference — When the compiler rejects a type in a concurrent context, check whether it is
Send,Sync, or both. The table above covers the most common cases. - Wrap !Sync types in Mutex for thread sharing — If you need to share a
!Synctype (likeCellorRefCell) across threads, wrap it inMutexorRwLockto provide the missing synchronization.
Summary
T: Syncmeans&T: Send— shared references can be safely sent to other threads.Cell,RefCell,Rc, andUnsafeCellare!Syncbecause they allow unsynchronized interior mutation.Mutex<T>isSyncwhenT: Send, even ifTis!Sync— the lock provides synchronization.Arc<T>requiresT: Send + Sync; useArc<Mutex<T>>for types that are!Sync.- The
Send/Syncsystem is Rust's compile-time guarantee against data races.
Code Examples
use std::sync::{Arc, Mutex};
use std::thread;
fn assert_sync<T: Sync>() {}
fn assert_send<T: Send>() {}
fn main() {
// T: Sync means &T: Send
assert_sync::<i32>();
assert_sync::<String>();
assert_sync::<Mutex<i32>>();
// Cell is !Sync — cannot be shared via &Cell across threads
// assert_sync::<std::cell::Cell<i32>>(); // compile error
// Arc requires T: Send + Sync
let shared = Arc::new(Mutex::new(vec![1, 2, 3]));
thread::scope(|s| {
for i in 0..3 {
let shared = Arc::clone(&shared);
s.spawn(move || {
let mut guard = shared.lock().unwrap();
guard.push(i * 10);
println!("thread {i}: {:?}", *guard);
});
}
});
println!("final: {:?}", *shared.lock().unwrap());
}