Rust 64 🦀 Sync and Send Traits for Multi-Threaded Safety
Rust’s concurrency story rests on two marker traits that have no methods, no runtime cost, and no counterpart in most other languages: Send and Sync. Everything else in this chapter series—threads, channels, mutexes—ultimately depends on the compiler’s ability to reason about which types can cross thread boundaries and which cannot. These two traits are how that reasoning is encoded.
The design is unusual in its minimalism. Send and Sync are auto traits: the compiler implements them automatically for any type composed of parts that already satisfy them . You almost never write impl Send for MyType in ordinary application code. Instead, you write structs and enums from Send and Sync components, and the compiler derives the properties for you. When the derivation fails—when you try to move an Rc<T> into a spawned thread, for example—the error message tells you exactly which type broke the chain and why .
What makes these traits powerful is what they prevent. Without Send and Sync, the Rust type system would still enforce memory safety within a single thread, but concurrent code would be a free-for-all. You could clone an Rc, send it to another thread, and corrupt the reference count silently. You could share a RefCell and trigger undefined behavior through unsynchronized borrow flags. Send and Sync close those doors at compile time.
This chapter covers what each trait means, how the compiler derives them, which standard-library types are and are not thread-safe, why the &T is Send if T is Sync relationship matters, and when manual unsafe impl is justified. By the end you will know not just which types to use but why the compiler rejects the ones it does.
Key point: Send means ownership of a value can be transferred to another thread; Sync means shared references (&T) to a value can be used from multiple threads safely. The precise relationship is: T: Sync if and only if &T: Send.
Why Send and Sync exist
The problem of unsynchronized sharing. Rust’s ownership system prevents data races within a single thread by enforcing that mutable references are exclusive and that values have exactly one owner at a time. But threads break this model: two threads can own clones of the same Rc<T>, both can attempt to decrement the reference count simultaneously, and the non-atomic count can be corrupted . Send and Sync extend the ownership discipline to the threaded case.
Auto traits and structural derivation. Most traits in Rust require explicit impl blocks. Send and Sync are different: they are auto traits, meaning the compiler automatically implements them for any type whose components all implement them . A struct containing an i32 and a String is automatically Send and Sync because both fields are. A struct containing an Rc<T> is automatically neither, because Rc is neither. This structural derivation makes the common case trivial and the exceptional case loud.
The &T relationship. Sync is defined in terms of Send: a type T is Sync if and only if &T is Send . This definition is precise and worth internalizing. It means that if you can send an immutable reference to another thread safely, the type is Sync. Mutex<T> is Sync because &Mutex<T> can be sent safely—the mutex itself handles synchronization internally .
UnsafeCell and interior mutability. The core of the Sync exclusion is UnsafeCell, the only type for which Rust does not guarantee no aliasing mutation. Cell<T>, RefCell<T>, and Rc<T> all contain UnsafeCell internally, which is why none of them are Sync . They provide interior mutability without synchronization, which is safe single-threaded but undefined behavior when shared across threads.
The unsafe contract. Send and Sync are unsafe traits. Implementing them manually is a promise to the compiler and to all other unsafe code that your type upholds the thread-safety invariant . The compiler cannot verify this promise; it trusts you. This is why manual implementations are rare and why incorrect ones can cause undefined behavior.
The separation of moving and sharing. Send and Sync are related but not interchangeable. Send is about moving ownership: can this value be transferred to another thread? Sync is about sharing references: can multiple threads hold &T simultaneously? A type can be Send but not Sync (e.g., a type with interior mutability that is safe to move but not to share), and in theory can be Sync but not Send (though this is rare in practice) .
a. Send: Transferring ownership across threads
Send marks a type whose values can be moved from one thread to another. The standard library definition is terse: “Types that can be transferred across thread boundaries” .
The key examples from the standard library illustrate the boundary. Primitive types like i32, f64, bool, and char are Send. Compound types built entirely from Send components are automatically Send . Most standard library collections—Vec<T>, HashMap<K, V>, Box<T>—are Send when their contents are .
The canonical exception is Rc<T>. It is not Send because its reference count is a plain non-atomic integer. If two threads cloned an Rc pointing to the same allocation, they could interleave their reference-count updates and corrupt the count, potentially freeing the allocation while the other thread still holds a reference . The compiler rejects any attempt to send an Rc across a thread boundary at compile time.
use std::rc::Rc;
use std::thread;
fn main() {
let shared = Rc::new(5);
let cloned = Rc::clone(&shared);
// This will not compile: Rc<i32> is not Send
thread::spawn(move || {
println!("{}", cloned);
});
}
The error message is specific: “Rc<i32> cannot be sent between threads safely.” The fix is Arc<T>, the atomic reference-counted pointer, which is Send when T is Send and Sync .
The Send bound appears throughout the standard library’s threading API. thread::spawn requires its closure to be Send + 'static, because the closure and its captures move into the new thread . The same bound appears on Sender<T> in mpsc, on JoinHandle<T>, and on most async runtime APIs.
b. Sync: Sharing references across threads
Sync is defined in terms of Send: a type T is Sync if and only if &T is Send . In plain terms, Sync means it is safe for multiple threads to hold immutable references to the same value simultaneously.
The structural derivation works the same way as Send: if all components of a type are Sync, the type is Sync . Primitive types are Sync. Box<T>, Vec<T>, and most collections are Sync when their contents are .
The interesting cases are the types with interior mutability. Cell<T> and RefCell<T> are not Sync because they allow mutation through a shared reference without synchronization . If thread A holds &RefCell<T> and calls borrow_mut(), while thread B holds another &RefCell<T> and calls borrow(), the runtime borrow flag—a plain Cell<isize>—can be corrupted. The compiler rejects this by making RefCell<T> not Sync.
Mutex<T> and RwLock<T> are Sync. They provide interior mutability but serialize access through locks, so shared references are safe . This is why Arc<Mutex<T>> is the canonical pattern for shared mutable state: Arc provides shared ownership (Send + Sync when T is), and Mutex provides synchronized interior mutability .
The &mut T case is subtle and worth noting. A mutable reference &mut T is Sync if T is Sync, even though it seems like it should allow unsynchronized mutation . The resolution is that a &mut T behind a shared reference (i.e., &&mut T) becomes read-only; you cannot mutate through it. So there is no data race risk.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0u32));
let handles: Vec<_> = (0..10)
.map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
*c.lock().unwrap() += 1;
})
})
.collect();
for h in handles {
h.join().unwrap();
}
println!("{}", *counter.lock().unwrap());
}
Here Arc<Mutex<u32>> is both Send and Sync. Each thread receives a clone of the Arc, and the Mutex serializes the increments. The compiler accepts this because both Arc and Mutex satisfy the bounds.
c. The relationship between the traits and common patterns
The precise relationship between Send and Sync can be summarized in four implications from the standard library :
&TisSendif and only ifTisSync&mut TisSendif and only ifTisSend&TisSyncif and only ifTisSync&mut TisSyncif and only ifTisSync
The Arc<T> implementation encodes the practical consequence of these rules. Arc<T> is Send if T: Send + Sync. It is Sync if T: Send + Sync . The requirement for both bounds is deliberate: Arc shares ownership, so if T were Send but not Sync, multiple threads could hold references to a value that is not safe to share, leading to data races. The Send bound on Arc<T>: Sync ensures that the value itself can move between threads when the last reference does.
Mutex<T> is Sync if T: Send . The Send bound on T is required because Mutex hands out &mut T access under the lock, and that mutable access could be used to move T out or replace it with a non-Send value. The mutex does not require T: Sync because it serializes all access; no two threads can touch the data simultaneously.
Channels follow the same pattern. Sender<T> and Receiver<T> are Send when T: Send. The receiver is not Sync (you cannot share a receiver across threads), which aligns with the single-consumer design .
Complete Example Session
The following session builds a series of functions that demonstrate Send and Sync in practice, including type-level assertions, the Rc failure, the Arc fix, and the structural derivation of both traits.
// ============================================
// PART 1: IMPORTS AND TYPE ASSERTION HELPERS
// ============================================
use std::rc::Rc;
use std::cell::RefCell;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
// Compile-time assertions that a type is Send/Sync
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
// ============================================
// PART 2: PRIMITIVES ARE SEND AND SYNC
// ============================================
fn primitives_are_send_sync() {
assert_send::<i32>();
assert_send::<f64>();
assert_send::<bool>();
assert_sync::<i32>();
assert_sync::<f64>();
assert_sync::<bool>();
}
// ============================================
// PART 3: COMPOSITE TYPES ARE AUTOMATICALLY SEND/SYNC
// ============================================
struct Point { x: i32, y: i32 }
struct Pair<T, U> { first: T, second: U }
fn composites_are_auto() {
assert_send::<Point>();
assert_sync::<Point>();
assert_send::<Pair<i32, String>>();
assert_sync::<Pair<i32, String>>();
}
// ============================================
// PART 4: RC IS NEITHER SEND NOR SYNC
// ============================================
fn rc_is_not_thread_safe() {
// These lines would not compile:
// assert_send::<Rc<i32>>();
// assert_sync::<Rc<i32>>();
//
// Error: `Rc<i32>` cannot be sent between threads safely
println!("Rc<i32>: not Send, not Sync");
}
// ============================================
// PART 5: REFCELL IS SEND (IF T: SEND) BUT NOT SYNC
// ============================================
fn refcell_is_send_not_sync() {
assert_send::<RefCell<i32>>();
// assert_sync::<RefCell<i32>>(); // would not compile
println!("RefCell<i32>: Send, not Sync");
}
// ============================================
// PART 6: ARC IS SEND AND SYNC (IF T IS)
// ============================================
fn arc_is_thread_safe() {
assert_send::<Arc<i32>>();
assert_sync::<Arc<i32>>();
assert_send::<Arc<Mutex<i32>>>();
assert_sync::<Arc<Mutex<i32>>>();
}
// ============================================
// PART 7: MUTEX IS SYNC (IF T: SEND)
// ============================================
fn mutex_is_sync() {
assert_sync::<Mutex<i32>>();
assert_send::<Mutex<i32>>();
}
// ============================================
// PART 8: SHARING WITH ARC<MUTEX<T>>
// ============================================
fn share_with_arc_mutex() -> u32 {
let counter = Arc::new(Mutex::new(0u32));
let handles: Vec<_> = (0..10)
.map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
for _ in 0..100 {
*c.lock().unwrap() += 1;
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
*counter.lock().unwrap()
}
// ============================================
// PART 9: SHARING WITH ARC<RWLock<T>>
// ============================================
fn share_with_arc_rwlock() -> Vec<i32> {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let readers: Vec<_> = (0..5)
.map(|_| {
let d = Arc::clone(&data);
thread::spawn(move || {
let guard = d.read().unwrap();
guard.iter().sum::<i32>()
})
})
.collect();
readers.into_iter().map(|h| h.join().unwrap()).collect()
}
// ============================================
// PART 10: MAIN
// ============================================
fn main() {
primitives_are_send_sync();
composites_are_auto();
rc_is_not_thread_safe();
refcell_is_send_not_sync();
arc_is_thread_safe();
mutex_is_sync();
println!("arc<mutex> counter: {}", share_with_arc_mutex());
println!("arc<rwlock> sums: {:?}", share_with_arc_rwlock());
}
The ten parts cover compile-time assertions for primitives, automatic derivation for composite types, the Rc exclusion, the Send-but-not-Sync case of RefCell, the thread-safe Arc and Mutex combinations, and two working examples of shared state. What the session demonstrates is that Send and Sync are not things you implement—they are things the compiler derives from the components you choose, and the choice of component determines thread safety.
Quick Reference
Trait Definitions
| Trait | Definition | Practical Meaning |
|---|---|---|
Send | Safe to transfer ownership to another thread | Value can move across threads |
Sync | &T is Send | Shared references can cross threads |
Send + Sync | Both properties | Safe to move and share |
!Send + !Sync | Neither | Single-threaded only |
Reference Implications
| Expression | Send if | Sync if |
|---|---|---|
T | T: Send | T: Sync |
&T | T: Sync | T: Sync |
&mut T | T: Send | T: Sync |
Box<T> | T: Send | T: Sync |
Standard Library Types
| Type | Send | Sync | Reason |
|---|---|---|---|
i32, f64, bool | ✅ | ✅ | Primitives |
String, Vec<T> | ✅ | ✅ | Owned data |
Rc<T> | ❌ | ❌ | Non-atomic refcount |
RefCell<T> | ✅ | ❌ | Unsynchronized borrow flag |
Cell<T> | ✅ | ❌ | Unsynchronized interior mutability |
Arc<T> | ✅ | ✅ | Atomic refcount |
Mutex<T> | ✅ | ✅ | Lock-serialized access |
RwLock<T> | ✅ | ✅ | Lock-serialized access |
| Raw pointers | ❌ | ❌ | No safety guards |
Composite Derivation Rules
| Component | Resulting Type |
|---|---|
All fields Send | Struct is Send |
All fields Sync | Struct is Sync |
One field !Send | Struct is !Send |
One field !Sync | Struct is !Sync |
Common Thread-Safe Combinations
| Combination | Send | Sync | Use Case |
|---|---|---|---|
Arc<T> | T: Send + Sync | T: Send + Sync | Shared ownership |
Arc<Mutex<T>> | T: Send | T: Send | Shared mutable state |
Arc<RwLock<T>> | T: Send | T: Send | Read-heavy shared state |
Mutex<T> alone | T: Send | T: Send | Interior mutability |
Best Practices
✅ Do This:
let x = Arc::new(Mutex::new(0)); // ✅ Arc for sharing
let y = Arc::clone(&x); thread::spawn(move || ...); // ✅ clone Arc per thread
assert_send::<MyType>(); // ✅ compile-time check
#[derive(Clone)] struct Config { ... } // ✅ auto derives
let data = Arc::new(vec![1, 2, 3]); // ✅ Send + Sync if T is
let handle = thread::spawn(move || r); // ✅ R: Send + 'static
❌ Don’t Do This:
let rc = Rc::new(5); thread::spawn(move || use(rc)); // ❌ Rc not Send
let cell = RefCell::new(0); share_across_threads(cell); // ❌ RefCell not Sync
unsafe impl Send for MyType {} // ❌ unless proven safe
struct Bad { r: Rc<i32> } // ❌ poisons Send/Sync
let x = Mutex::new(0); let y = x; thread::spawn(...); // ❌ moved, not shared
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Rc cannot cross threads | Non-atomic refcount | Use Arc instead |
RefCell cannot be shared | Unsynchronized borrow flag | Use Mutex or RwLock |
Closure not Send | Captures non-Send type | Ensure all captures are Send |
Arc<T> not Send | T not Send + Sync | Make T satisfy both bounds |
| Manual impl causes UB | Incorrect safety promise | Avoid unless you have a proof |
| Raw pointer in struct | Raw pointers are !Send !Sync | Encapsulate behind safe abstraction |
'static bound failure | Closure captures a reference | Use move and owned data |
Real-World Examples
1. Arc for shared ownership
let data = Arc::new(vec![1, 2, 3]);
let d = Arc::clone(&data);
thread::spawn(move || { println!("{:?}", d); });
2. Arc for shared mutable state
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
thread::spawn(move || { *c.lock().unwrap() += 1; });
3. Assert Send for API bounds
fn spawn_task<F: Send + 'static>(f: F) { thread::spawn(f); }
4. Struct with Send fields is Send
#[derive(Clone)] struct Config { timeout: u64, name: String }
// Config is automatically Send + Sync
5. RefCell is Send but not Sync
let cell = RefCell::new(0); // Send (if i32), not Sync
6. Mutex is Sync
fn assert_sync<T: Sync>() {} assert_sync::<Mutex<i32>>();
7. Channel endpoints
let (tx, rx) = mpsc::channel::<i32>();
// tx: Send, rx: Send (when T: Send)
8. Raw pointers block auto-derivation
struct Raw { ptr: *const u8 } // !Send, !Sync
9. Arc for read-heavy
let cache = Arc::new(RwLock::new(HashMap::new()));
10. Compile-time safety check
fn is_thread_safe<T: Send + Sync>() {}
is_thread_safe::<Arc<Mutex<Vec<i32>>>>();
Visual
Send and Sync Relationship
┌──────────────────────────────────────────────┐
│ T: Send → ownership can move │
│ T: Sync → &T can be shared │
│ │
│ Definition: T: Sync ⇔ &T: Send │
│ │
│ ┌───────────────┬───────────┬────────────┐ │
│ │ Type │ Send? │ Sync? │ │
│ ├───────────────┼───────────┼────────────┤ │
│ │ i32, String │ ✅ │ ✅ │ │
│ │ Rc<T> │ ❌ │ ❌ │ │
│ │ RefCell<T> │ ✅ │ ❌ │ │
│ │ Arc<T> │ ✅* │ ✅* │ │
│ │ Mutex<T> │ ✅ │ ✅ │ │
│ └───────────────┴───────────┴────────────┘ │
│ * when T: Send + Sync │
└──────────────────────────────────────────────┘
Auto Trait Derivation
┌──────────────────────────────────────────────┐
│ struct Point { x: i32, y: i32 } │
│ │
│ i32: Send + Sync │
│ i32: Send + Sync │
│ │ │
│ ▼ │
│ All fields Send + Sync │
│ │ │
│ ▼ │
│ Point: Send + Sync automatically │
│ │
│ ──────────────────────────────────────── │
│ │
│ struct Bad { r: Rc<i32> } │
│ │
│ Rc<i32>: !Send + !Sync │
│ │ │
│ ▼ │
│ Bad: !Send + !Sync automatically │
└──────────────────────────────────────────────┘
Thread Safety Matrix
┌──────────────────────────────────────────────┐
│ │ Sync │ !Sync │
│ ────────────┼───────────┼───────────────────│
│ Send │ ✅ Use │ ⚠️ Move only │
│ │ Arc<T> │ (not share) │
│ ────────────┼───────────┼───────────────────│
│ !Send │ ⚠️ Rare │ ❌ Single-thread │
│ │ (theoretical) │
│ ────────────┴───────────┴───────────────────│
│ │
│ ✅ Send + Sync: Arc, Mutex, primitives │
│ ⚠️ Send + !Sync: RefCell, Cell │
│ ❌ !Send + !Sync: Rc, raw pointers │
└──────────────────────────────────────────────┘
Arc and Mutex Bounds
┌──────────────────────────────────────────────┐
│ Arc<T>: Send + Sync │
│ requires T: Send + Sync │
│ │
│ Mutex<T>: Send + Sync │
│ requires T: Send │
│ (no Sync bound: lock serializes access) │
│ │
│ Arc<Mutex<T>>: Send + Sync │
│ requires T: Send │
│ │
│ Channels: Sender<T>, Receiver<T> │
│ Send when T: Send │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
Send | Ownership can transfer across threads |
Sync | &T can be shared across threads |
| Relationship | T: Sync ⇔ &T: Send |
| Auto trait | Derived structurally, no manual impl |
| Unsafe to implement | Yes, promise to the compiler |
Rc<T> | !Send, !Sync (non-atomic refcount) |
Arc<T> | Send + Sync if T: Send + Sync |
RefCell<T> | Send but !Sync |
Mutex<T> | Send + Sync if T: Send |
| Raw pointers | !Send, !Sync |
| Thread spawn bound | F: Send + 'static |
Key takeaways:
Sendis about moving,Syncis about sharing. ASendtype can move to another thread; aSynctype can be referenced from multiple threads simultaneously .- The definition is precise.
T: Syncif and only if&T: Send. This relationship explains whyMutex<T>isSyncbutRefCell<T>is not . - Auto traits derive structurally. If all components are
Send, the type isSend. If all areSync, the type isSync. You almost never implement them manually . Rc<T>is neither. The non-atomic reference count makes it unsafe to move or share.Arc<T>is the thread-safe replacement .- Interior mutability breaks
Sync.Cell<T>,RefCell<T>, andUnsafeCell<T>are notSyncbecause they allow unsynchronized mutation through shared references . Mutex<T>andRwLock<T>restoreSync. They serialize access, making shared references safe even with interior mutability .- Manual implementations are unsafe and rare. An
unsafe impl Sendis a promise the compiler cannot verify. Only write one with a clear safety argument . - The compiler rejects unsafe sharing. When you try to send an
Rcor share aRefCell, the error is not a bug—it is the type system preventing undefined behavior.
Remember: Send and Sync are the compiler’s thread-safety oracle. Send means a value can move to another thread; Sync means multiple threads can hold references to it. The compiler derives both automatically from a type’s components, so the way to make a type thread-safe is to build it from thread-safe parts. Rc, Cell, and RefCell are excluded because they provide unsynchronized sharing; Arc, Mutex, and RwLock are included because they synchronize it. When you see a Send or Sync error, read it as the compiler telling you that your data structure violates a concurrency invariant—and reach for the synchronized alternative rather than unsafe impl.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!