When sharing a reference breaks the rules
You're building a concurrent app. You have a struct that holds some configuration and a cache. You spawn a worker thread to process a request. You capture a reference to your shared state in the closure and pass it to thread::spawn. The compiler rejects you with the trait Sync is not implemented for MyState. You stare at the error. Your struct looks harmless. It's just a few integers, a string, and maybe an Rc to a shared config. Why does Rust think it's unsafe to share?
The error isn't about your data being dangerous. It's about how your data tracks itself. Rust allows you to share a reference to a type across threads only if the type guarantees that sharing won't cause data races. That guarantee is the Sync trait. When the compiler says Sync is not implemented, it's telling you that your type contains a component that can mutate through a shared reference without coordination. Two threads could read and write the same memory at the same time, and the compiler won't let that happen.
What Sync actually means
Sync is a marker trait. It doesn't have methods. It signals a property of the type. If T is Sync, then &T is Send. In plain words: a reference to T can be sent to another thread.
This definition is precise. Sync is about references. Send is about ownership. A type can be Send without being Sync. A type can be Sync without being Send. Most types are both. The compiler derives Sync automatically for any struct where all fields are Sync. You rarely write impl Sync yourself. You usually get the error because a field in your struct is not Sync.
The usual culprits are RefCell<T> and Rc<T>. Both types allow mutation through a shared reference. RefCell enforces borrowing rules at runtime. Rc tracks how many owners exist. Both mechanisms break down when multiple threads access the same instance simultaneously.
The interior mutability trap
Interior mutability is the ability to change the contents of a type even when you only hold a shared reference. In single-threaded code, this is useful. You can bypass the borrow checker's static analysis and check rules at runtime. In multi-threaded code, interior mutability is a minefield.
RefCell<T> stores a borrow flag. When you borrow immutably, the flag increments. When you borrow mutably, the flag checks that no one else is borrowing. The flag is a plain integer. It lives in thread-local storage. If thread A checks the flag and thread B checks the flag at the same time, both might see "no borrows" and both proceed to mutate. The runtime check fails to prevent the race. Rust marks RefCell as not Sync to stop this at compile time.
Rc<T> stores a reference count. When you clone an Rc, the count increments. When an Rc drops, the count decrements. The count is a plain usize. Incrementing and decrementing a usize is not atomic. If two threads clone an Rc simultaneously, the counter can get corrupted. The value might be freed while a thread still holds a reference. Or the memory might leak. Rust marks Rc as not Sync for the same reason.
Minimal example
Here's a struct that triggers the error. It uses Rc to share a string. The code tries to send a reference to a new thread.
use std::rc::Rc;
use std::thread;
struct Config {
// Rc is not Sync. It uses a non-atomic counter.
name: Rc<String>,
}
fn main() {
let config = Config {
name: Rc::new("server".into()),
};
// The closure captures a reference to config.
// thread::spawn requires the closure to be Send.
// A closure capturing &T is Send only if T is Sync.
thread::spawn(|| {
println!("Config: {}", config.name);
});
}
The compiler rejects this with E0277 (the trait bound Config: Sync is not satisfied). The error points to the thread::spawn call. It explains that Config is not Sync because Rc<String> is not Sync. The chain of reasoning is clear. Rc breaks the Sync contract. The struct inherits that breakage.
Walkthrough of the fix
To share data across threads, you need thread-safe alternatives. Rc becomes Arc. RefCell becomes Mutex or RwLock. Arc stands for Atomic Reference Counted. It uses atomic operations to update the counter. Atomic operations are safe across threads. Mutex provides mutual exclusion. Only one thread can access the inner data at a time.
Replace Rc with Arc. If you need mutation, wrap the data in a Mutex.
use std::sync::{Arc, Mutex};
use std::thread;
struct Config {
// Arc is thread-safe shared ownership.
// Mutex protects the inner data for safe mutation.
name: Arc<Mutex<String>>,
}
fn main() {
let config = Config {
name: Arc::new(Mutex::new("server".into())),
};
// Clone the Arc to share ownership with the thread.
// Arc::clone is the convention to signal a shallow clone.
let config_clone = Arc::clone(&config.name);
thread::spawn(move || {
// Lock the mutex before accessing the data.
// The lock guard ensures exclusive access.
let name = config_clone.lock().unwrap();
println!("Config: {}", name);
});
}
The code compiles. Arc implements Sync. Mutex implements Sync. The struct derives Sync. The closure captures an Arc, which is Send. The thread can run safely.
Realistic example
Consider a cache that multiple threads need to read and update. A naive implementation might use RefCell for interior mutability. That works in a single thread. It fails immediately when you introduce concurrency.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
struct Cache {
// Arc allows shared ownership across threads.
// Mutex ensures only one thread modifies the map at a time.
data: Arc<Mutex<HashMap<String, Vec<u8>>>>,
}
impl Cache {
fn new() -> Self {
Cache {
data: Arc::new(Mutex::new(HashMap::new())),
}
}
fn get(&self, key: &str) -> Option<Vec<u8>> {
// Lock the mutex to read the map.
// Clone the value to avoid holding the lock.
let map = self.data.lock().unwrap();
map.get(key).cloned()
}
fn insert(&self, key: String, value: Vec<u8>) {
// Lock the mutex to write to the map.
let mut map = self.data.lock().unwrap();
map.insert(key, value);
}
}
fn main() {
let cache = Cache::new();
let mut handles = vec![];
// Spawn threads that share the cache.
for i in 0..4 {
// Clone the Arc inside the cache struct.
// This bumps the atomic reference count.
let cache_clone = cache.data.clone();
let handle = thread::spawn(move || {
let key = format!("key_{}", i);
cache_clone.lock().unwrap().insert(key, vec![i]);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Cache size: {}", cache.data.lock().unwrap().len());
}
The Cache struct holds an Arc<Mutex<HashMap>>. The Arc handles ownership. The Mutex handles synchronization. Threads clone the Arc to get their own handle. They lock the Mutex to access the map. The compiler is happy. The code is safe.
Convention aside: use Arc::clone(&value) instead of value.clone(). Both compile. Both work. The explicit form signals to readers that you're cloning the pointer, not the data. It prevents confusion with deep clones.
Pitfalls and edge cases
The error can hide in subtle places. Wrapping a non-Sync type in a Mutex doesn't always fix it. The wrapper itself must be Sync.
Rc<Mutex<T>> is not Sync. The Mutex protects the inner data, but the Rc counter is still non-atomic. Two threads cloning the Rc can corrupt the count. You need Arc<Mutex<T>>.
Arc<RefCell<T>> is not Sync. The Arc is thread-safe, but the RefCell inside isn't. The borrow flag in RefCell isn't atomic. You need Arc<Mutex<T>> or Arc<RwLock<T>>.
Sync is not Send. A type can be Sync but not Send. Raw pointers are a classic example. *const T is Send but not Sync. You can move a raw pointer to another thread. You cannot share a reference to a raw pointer across threads. The compiler enforces this distinction. If you see Sync errors, check for raw pointers or Cell types. Cell<T> is not Sync and not Send. It's strictly single-threaded.
Sometimes you need to implement Sync manually. This happens with FFI. A C library might provide a handle that is actually thread-safe, but Rust doesn't know that. You can assert Sync with unsafe impl.
struct CHandle(*mut std::ffi::c_void);
// SAFETY:
// 1. The C library documentation guarantees thread-safety for CHandle.
// 2. We do not expose mutable access to the handle through shared references.
// 3. All methods on CHandle acquire internal locks in the C code.
unsafe impl Sync for CHandle {}
Treat the SAFETY comment as a proof. If you can't write the invariants, you don't have a proof. Don't implement Sync just to make the error go away. The compiler is protecting you from undefined behavior.
Decision matrix
Use Arc<T> when you need shared ownership across threads and the inner type is Sync. Use Arc<Mutex<T>> when you need shared ownership and mutable access across threads. Use Arc<RwLock<T>> when you have many readers and few writers, and you want to allow concurrent reads. Use AtomicUsize or AtomicBool when you need to share a single primitive value without the overhead of a lock. Use Rc<T> when you have shared ownership but only within a single thread. Use RefCell<T> when you need interior mutability but only within a single thread. Reach for Send markers when you need to move ownership to another thread; Sync is about sharing references.
Don't fight the compiler here. Reach for Arc. Trust the borrow checker. It usually has a point.