Rust Send and Sync Traits
Introduction
When writing concurrent code in Rust, you'll encounter two special traits: Send
and Sync
. These traits are fundamental to Rust's concurrency model and are a key part of how Rust guarantees thread safety. Unlike most traits in Rust, you rarely implement these traits directly—they're automatically derived for most types, but understanding them is crucial for writing correct concurrent code.
In this guide, we'll explore:
- What the
Send
andSync
traits are - How they relate to Rust's ownership system
- Common patterns for working with these traits
- Real-world examples showing how they're used
What Are Send and Sync?
The Send Trait
A type is Send
if it can be safely transferred between threads. In other words, a type T
is Send
if it's safe to move a value of type T
from one thread to another.
// This is how Send is defined in the standard library
pub unsafe trait Send {
// Empty trait
}
The Send
trait is a marker trait, which means it doesn't have any methods to implement. It simply marks types that can be safely sent between threads.
The Sync Trait
A type is Sync
if it can be safely shared between threads. More precisely, a type T
is Sync
if and only if a shared reference to that type (&T
) is Send
.
// This is how Sync is defined in the standard library
pub unsafe trait Sync {
// Empty trait
}
Like Send
, Sync
is also a marker trait with no methods.
How Rust Uses Send and Sync
These traits are integral to Rust's concurrency guarantees:
-
The compiler enforces thread safety: If you try to send a non-
Send
type across threads, or share a non-Sync
type between threads, your code won't compile. -
Most types are automatically Send and Sync: The Rust compiler automatically implements these traits for types that meet the safety criteria.
-
Some types are intentionally not Send or Sync: For example,
Rc<T>
(a reference-counted pointer) is neitherSend
norSync
because its reference counting isn't atomic.
Let's visualize the relationship:
Common Examples
Let's look at some common Rust types and their Send
and Sync
status:
Type | Send | Sync | Explanation |
---|---|---|---|
i32 , String , most standard types | ✅ | ✅ | Basic types with no interior mutability are both Send and Sync |
Rc<T> | ❌ | ❌ | Reference counting is not thread-safe |
Arc<T> where T: Send + Sync | ✅ | ✅ | Atomic reference counting is thread-safe |
Cell<T> , RefCell<T> | ✅ | ❌ | Interior mutability without synchronization |
Mutex<T> where T: Send | ✅ | ✅ | Provides synchronized access to data |
RwLock<T> where T: Send + Sync | ✅ | ✅ | Provides synchronized read/write access |
Raw pointers (*const T , *mut T ) | ✅ | ❌ | No safety guarantees for sharing |
Practical Examples
Example 1: Thread-Safe Counter
Let's create a simple counter that can be safely shared between threads:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Create a counter wrapped in Arc (for sharing) and Mutex (for thread safety)
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
// Spawn 10 threads
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// Lock the mutex and increment the counter
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
// Wait for all threads to complete
for handle in handles {
handle.join().unwrap();
}
// Print the final value
println!("Final count: {}", *counter.lock().unwrap());
}
Output:
Final count: 10
In this example:
Arc
lets us share the counter between threads (it's bothSend
andSync
)Mutex
provides synchronized access to the counter
Example 2: Understanding Non-Send Types
Let's see what happens when we try to send a non-Send
type across threads:
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(42);
thread::spawn(move || {
// Try to use data in another thread
println!("The answer is: {}", *data);
});
}
This code won't compile, and you'll get an error like:
error[E0277]: `Rc<i32>` cannot be sent between threads safely
--> src/main.rs:6:5
|
6 | thread::spawn(move || {
| ^^^^^^^^^^^^^ `Rc<i32>` cannot be sent between threads safely
|
= help: the trait `Send` is not implemented for `Rc<i32>`
The compiler prevents us from sending an Rc<T>
across threads because its reference counting isn't thread-safe.
Example 3: Thread-Safe Version Using Arc
Here's the correct way to share data between threads:
use std::sync::Arc;
use std::thread;
fn main() {
// Arc is the thread-safe alternative to Rc
let data = Arc::new(42);
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
// Now we can use data_clone in another thread
println!("The answer is: {}", *data_clone);
});
handle.join().unwrap();
}
Output:
The answer is: 42
This works because Arc<T>
is both Send
and Sync
(as long as T
is Send
and Sync
).
Implementing Send and Sync
Generally, you don't need to implement these traits manually. They're automatically derived for types composed of Send
and Sync
types.
However, sometimes you might need to:
- Opt out of automatic implementation:
use std::marker::PhantomData;
use std::cell::Cell;
// This makes MyType not Send and not Sync
struct MyType<T> {
data: T,
// PhantomData<*const ()> is neither Send nor Sync
_marker: PhantomData<*const ()>,
}
- Manually implement Send/Sync (use with caution!):
use std::marker::{Send, Sync};
struct MyType {
// Contains raw pointers or other non-Send/Sync types
// but you guarantee thread safety through other means
data: *mut i32,
}
// SAFETY: MyType is used in a way that ensures thread safety
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}
Warning: Manually implementing Send
and Sync
is unsafe and requires you to guarantee that your implementation is thread-safe.
Send + Sync and Trait Bounds
When writing generic code that works with threads, you'll often need to specify trait bounds:
use std::thread;
fn process_in_thread<T: Send + 'static>(data: T) {
thread::spawn(move || {
// Do something with data
println!("Processing data in thread");
});
}
The Send + 'static
bound ensures that data
can be safely moved to the new thread and lives for the entire program.
Similarly, for sharing references between threads:
use std::thread;
use std::sync::Arc;
fn share_between_threads<T: Send + Sync + 'static>(data: &T) {
let data_ref = Arc::new(data);
let data_clone = Arc::clone(&data_ref);
thread::spawn(move || {
// Use data_clone
});
}
Common Pitfalls and Solutions
1. Using Rc
instead of Arc
Problem:
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(vec![1, 2, 3]);
// Doesn't compile - Rc is not Send
thread::spawn(move || {
println!("{:?}", data);
});
}
Solution: Use Arc
instead of Rc
for thread-safe reference counting.
2. Sharing mutable data without synchronization
Problem:
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);
thread::spawn(move || {
// Cannot mutate through Arc - doesn't compile
data_clone.push(4);
});
}
Solution: Use Mutex
or RwLock
for synchronized mutable access.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let data_clone = Arc::clone(&data);
thread::spawn(move || {
// Lock the mutex and then modify
data_clone.lock().unwrap().push(4);
});
}
3. Unexpected non-Send types in a struct
Problem:
use std::thread;
use std::cell::RefCell;
struct MyData {
counter: RefCell<i32>, // RefCell is not Sync!
}
fn main() {
let data = MyData { counter: RefCell::new(0) };
// Doesn't compile - MyData is not Sync because RefCell isn't
thread::spawn(move || {
*data.counter.borrow_mut() += 1;
});
}
Solution: Use thread-safe alternatives.
use std::sync::{Arc, Mutex};
use std::thread;
struct MyData {
counter: Mutex<i32>, // Mutex is Sync!
}
fn main() {
let data = Arc::new(MyData { counter: Mutex::new(0) });
let data_clone = Arc::clone(&data);
thread::spawn(move || {
*data_clone.counter.lock().unwrap() += 1;
});
}
Summary
Rust's Send
and Sync
traits are foundational to its concurrency story:
Send
: Types that can be safely transferred between threadsSync
: Types that can be safely shared between threads (via references)
Most Rust types automatically implement these traits, but understanding when they don't (and why) helps you write correct concurrent code. The compiler uses these traits to prevent data races at compile time, making Rust's concurrency both powerful and safe.
When writing concurrent Rust code, remember:
- Use
Arc
instead ofRc
for shared ownership between threads - Use synchronization primitives like
Mutex
andRwLock
for shared mutable state - Pay attention to compiler errors about
Send
andSync
— they're helping you avoid bugs!
Additional Resources
Exercises
- Create a thread-safe counter that can be incremented from multiple threads.
- Write a function that takes a closure and executes it in a new thread, ensuring proper type constraints.
- Implement a simple thread pool that can process tasks in parallel.
- Create a type that intentionally doesn't implement
Send
orSync
, and then wrap it in synchronization primitives to make it thread-safe. - Experiment with
Arc<Mutex<T>>
vs.Mutex<Arc<T>>
— how do they differ and when would you use each?
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)