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
SendandSynctraits 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-
Sendtype across threads, or share a non-Synctype 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 neitherSendnorSyncbecause 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:
Arclets us share the counter between threads (it's bothSendandSync)Mutexprovides 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
Arcinstead ofRcfor shared ownership between threads - Use synchronization primitives like
MutexandRwLockfor shared mutable state - Pay attention to compiler errors about
SendandSync— 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
SendorSync, 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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)