Sync and Send: Rust Thread Safety Marker Traits
Send and Sync are marker traits that enforce thread safety at compile time. Send indicates a type can be safely moved across thread boundaries; Sync indicates a type can be safely shared between threads (via immutable references). These traits are zero-method interfaces—they don't define behavior, only safety properties. The compiler automatically implements them for types composed of Send/Sync fields, and manually implements them (via unsafe) only when non-obvious safety guarantees hold. This design is the foundation of Rust's fearless concurrency.
Key Takeaways
Send= safe to move to another thread: a type implementingSendcan be transferred to a new thread without creating data races.Sync= safe to share between threads: a type implementingSynccan be accessed via immutable references from multiple threads without data races.- Compiler auto-implements both: Most primitive types (
i32,bool,String) are automaticallySendandSync; structs composed ofSend/Syncfields inherit both. - Notable exceptions:
Rc<T>is neitherSendnorSync(unsafe refcount across threads);RefCell<T>isSendbut notSync(non-atomic borrow tracking). - Manual implementation is rare and
unsafe: only implementSendorSyncmanually when you've verified thread safety guarantees the compiler cannot prove.
Prerequisites
Before you begin, you should be familiar with:
- Rust's threading model and how to spawn threads with
std::thread::spawn. - The concepts of ownership and borrowing in single-threaded contexts.
- Basic trait definitions and implementation.
- What unsafe code means and when it's necessary.
What Does Send Mean?
A type is Send if it is safe to move (transfer ownership) from one thread to another. When you move a value across a thread boundary, you're handing off complete ownership to the new thread. The original thread can no longer access that value.
Here's a simple example:
use std::thread;
fn main() {
let x = 5; // i32 is Send
thread::spawn(move || {
println!("x = {}", x); // Ownership transferred; x moved into this thread
});
// println!("{}", x); // ERROR: x was moved; original thread can't access it
}
The move keyword transfers ownership of x (an i32, which is Send) to the closure executed in the spawned thread. Since i32 implements Send, the compiler permits this transfer.
Conversely, Rc<T> (a reference-counted pointer) is not Send:
use std::rc::Rc;
use std::thread;
fn main() {
let rc = Rc::new(5);
thread::spawn(move || {
println!("{:?}", rc); // COMPILE ERROR: Rc<T> is not Send
});
}
Why? Rc's reference count is not atomic. If two threads both tried to decrement the count, a race condition could occur. Rust prevents this by making Rc non-Send.
What Does Sync Mean?
A type is Sync if it is safe to share between threads via immutable references. If type T is Sync, then &T can be safely shared across threads—multiple threads can hold &T simultaneously without data races.
Most primitive types are Sync:
use std::sync::Arc;
use std::thread;
fn main() {
let x = Arc::new(42); // Arc<i32> is Send and Sync
let x1 = x.clone();
let x2 = x.clone();
let t1 = thread::spawn(move || {
println!("Thread 1: {}", x1); // Shared, immutable reference
});
let t2 = thread::spawn(move || {
println!("Thread 2: {}", x2); // Shared, immutable reference
});
t1.join().unwrap();
t2.join().unwrap();
}
Arc<i32> (atomic reference count) is both Send and Sync, so both threads can safely hold shared references to the same integer.
Conversely, RefCell<T> is not Sync:
use std::cell::RefCell;
use std::sync::Arc;
use std::thread;
fn main() {
let rc = Arc::new(RefCell::new(5));
let rc1 = rc.clone();
thread::spawn(move || {
// COMPILE ERROR: RefCell is not Sync
*rc1.borrow_mut() = 10;
});
}
Why? RefCell's borrow tracking (the count of active mutable/immutable borrows) is not thread-safe. Two threads calling borrow_mut() simultaneously could corrupt the borrow state.
How Are Send and Sync Automatically Implemented?
Rust's compiler automatically implements Send and Sync for your types using a simple rule:
A struct is Send if all its fields are Send.
A struct is Sync if all its fields are Sync.
struct Point {
x: i32, // Send and Sync
y: i32, // Send and Sync
}
// Point is automatically Send and Sync (no manual impl needed)
struct Container<T> {
data: T,
}
// Container is Send if T is Send; Sync if T is Sync (generic auto-implementation)
struct BadContainer {
rc: Rc<i32>, // Rc is not Send
}
// BadContainer is NOT Send (it contains a non-Send field)
This auto-implementation is powerful and reduces boilerplate. For most types, you never think about Send or Sync—the compiler handles it automatically.
When Do You Manually Implement Send and Sync?
Manual implementation is rare and requires unsafe. You only implement Send or Sync manually when the compiler cannot prove thread safety but you have verified it through other means (e.g., internal synchronization primitives).
Example: Custom wrapper around a raw pointer
struct MyBox(*mut u8);
// UNSAFE: We claim that MyBox is safe to move across threads
// This is valid ONLY if the heap memory it points to is never shared
unsafe impl Send for MyBox {}
// UNSAFE: We claim that MyBox is safe to share via immutable references
// This requires that all access through &MyBox is synchronized
unsafe impl Sync for MyBox {}
fn main() {
let ptr = Box::into_raw(Box::new(42u8));
let mb = MyBox(ptr);
std::thread::spawn(move || {
unsafe { println!("{}", *mb.0); }
}).join().unwrap();
}
Key point: Manual implementation is a promise to the Rust compiler that you've verified thread safety that the compiler cannot. If your promise is wrong, you get undefined behavior—data races, corruption, crashes. Use unsafe sparingly and only when absolutely certain.
Frequently Asked Questions
Why are some standard-library types not Send or Sync?
Safety. Types like Rc<T> have non-atomic reference counts that would race if shared across threads. Types like RefCell<T> have non-atomic borrow state. Rust's designers chose correctness over convenience: better to reject unsafe code at compile time.
Can I make a non-Send type Send with a wrapper?
Not easily. You would need to add thread-safe synchronization inside the wrapper (e.g., wrapping Rc<T> in a Mutex<Rc<T>> adds Sync but not Send to the refcount itself). The better approach is to use the right type from the start: Arc<T> instead of Rc<T> for shared data.
What's the difference between Arc<T> and Rc<T>?
Arc = Atomic Reference Count. It uses atomic operations for the refcount, making it safe to share across threads. Rc = Reference Count. It uses non-atomic operations, making it fast but unsuitable for threads. Both are Send and Sync if T is Send and Sync.
If a type is Send, is it automatically Sync?
No. Send means it's safe to move; Sync means it's safe to share. A type can be Send but not Sync (e.g., RefCell<i32> is Send—you can move it to another thread—but not Sync—you can't safely share &RefCell<i32> across threads).
How do I test if a type is Send or Sync?
Use a compile-time assertion:
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn main() {
assert_send::<i32>(); // Compiles
assert_send::<Rc<i32>>(); // COMPILE ERROR
assert_sync::<Arc<i32>>(); // Compiles
}
Summary: When to Use What
| Type | Send | Sync | Typical Use |
|---|---|---|---|
Primitives (i32, bool, etc.) | ✓ | ✓ | Move or share freely |
Rc<T> | ✗ | ✗ | Single-threaded reference counting |
Arc<T> | ✓ | ✓ | Multi-threaded reference counting |
RefCell<T> | ✓ | ✗ | Single-threaded interior mutability |
Mutex<T> | ✓ (if T is Send) | ✓ (if T is Sync) | Multi-threaded interior mutability |
| Raw pointers | ✗ | ✗ | Unsafe-only, manual verification |
Key Takeaways (Recap)
Send and Sync are Rust's mechanism for encoding thread safety into the type system. Understand them, rely on the compiler's auto-implementation, and use unsafe only when you've verified guarantees the compiler cannot. This is fearless concurrency.