Understanding thread safety
Send and Sync are auto traits in Rust that determine whether types are safe to transfer or share across thread boundaries. These marker traits are fundamental to Rust's thread safety guarantees and enable compile-time verification of concurrent code correctness.
Send if it's safe to transfer ownership across thread boundariesSync if it's safe to share references across thread boundaries (T is Sync if &T is Send)When writing concurrent code, you need to ensure that:
Without Send and Sync, runtime data races could occur, leading to undefined behavior.
use std::thread;
use std::sync::{Arc, Mutex};
use std::rc::Rc;
// Example 1: Send types can be moved between threads
#[derive(Debug)]
struct SendableData {
value: i32,
name: String,
}
// Automatically implements Send because all fields are Send
fn example_send() {
let data = SendableData {
value: 42,
name: String::from("test"),
};
// OK: SendableData is Send, can move to another thread
let handle = thread::spawn(move || {
println!("Data in thread: {:?}", data);
});
handle.join().unwrap();
}
// Example 2: Sync types can be shared via references
fn example_sync() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let mut handles = vec![];
for i in 0..3 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut lock = data_clone.lock().unwrap();
lock.push(i);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final data: {:?}", data.lock().unwrap());
}
// Example 3: Rc is NOT Send or Sync
fn example_rc_not_send() {
let rc = Rc::new(42);
// ❌ Compile error: Rc is not Send
// let handle = thread::spawn(move || {
// println!("Value: {}", *rc);
// });
// ✅ Use Arc instead for thread-safe reference counting
let arc = Arc::new(42);
let handle = thread::spawn(move || {
println!("Value: {}", *arc);
});
handle.join().unwrap();
}
// Example 4: Raw pointers are NOT Send or Sync by default
struct NotSend {
ptr: *const i32, // Raw pointer is not Send
}
// Explicitly opt-out of Send (it's already not Send due to raw pointer)
// unsafe impl Send for NotSend {} // Don't do this unless you know it's safe!
// Example 5: PhantomData for correct auto-trait inference
use std::marker::PhantomData;
struct MyBox<T> {
ptr: *mut T,
_marker: PhantomData<T>, // Ensures MyBox<T> has same Send/Sync as T
}
// Now MyBox<T> is Send only if T is Send
// Now MyBox<T> is Sync only if T is Sync
unsafe impl<T: Send> Send for MyBox<T> {}
unsafe impl<T: Sync> Sync for MyBox<T> {}
fn main() {
example_send();
example_sync();
example_rc_not_send();
}
T is Send if transferring ownership to another thread is safeSend by defaultSend if all fields are SendRc, and RefCell are NOT SendT is Sync if &T is Send (immutable references can be shared)SyncMutex, RwLock)Cell and RefCell are NOT Sync because they use non-atomic operations// T is Sync if &T is Send
// Arc<T> is Send + Sync if T is Send + Sync
// Mutex<T> is Send + Sync if T is Send
// ❌ DON'T: Unsafe without proper analysis
struct MyType {
ptr: *mut i32,
}
// Dangerous! Only if you can guarantee thread safety
// unsafe impl Send for MyType {}
// unsafe impl Sync for MyType {}
// ✅ DO: Use proper synchronization
struct SafeType {
data: Arc<Mutex<i32>>,
}
// Automatically Send + Sync
// ❌ DON'T: RefCell is not Sync
// let shared = Arc::new(RefCell::new(vec![1, 2, 3]));
// Won't compile: RefCell is not Sync
// ✅ DO: Use Mutex for interior mutability across threads
let shared = Arc::new(Mutex::new(vec![1, 2, 3]));
// ❌ DON'T: Transmuting to bypass Send/Sync
use std::mem;
// fn unsafe_send<T>(value: T) -> Box<dyn Send> {
// unsafe { mem::transmute(Box::new(value)) }
// }
// ✅ DO: Respect the type system
fn safe_send<T: Send>(value: T) -> Box<dyn Send> {
Box::new(value)
}
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
/// Thread-safe cache with generic keys and values
/// K and V must be Send + Sync for the cache to be thread-safe
pub struct ThreadSafeCache<K, V>
where
K: Eq + std::hash::Hash + Send + Sync,
V: Clone + Send + Sync,
{
data: Arc<RwLock<HashMap<K, V>>>,
}
impl<K, V> ThreadSafeCache<K, V>
where
K: Eq + std::hash::Hash + Send + Sync,
V: Clone + Send + Sync,
{
pub fn new() -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn get(&self, key: &K) -> Option<V> {
let read_lock = self.data.read().unwrap();
read_lock.get(key).cloned()
}
pub fn insert(&self, key: K, value: V) {
let mut write_lock = self.data.write().unwrap();
write_lock.insert(key, value);
}
pub fn len(&self) -> usize {
self.data.read().unwrap().len()
}
}
// Clone is cheap because we're cloning Arc
impl<K, V> Clone for ThreadSafeCache<K, V>
where
K: Eq + std::hash::Hash + Send + Sync,
V: Clone + Send + Sync,
{
fn clone(&self) -> Self {
Self {
data: Arc::clone(&self.data),
}
}
}
// Example usage
fn cache_example() {
let cache = ThreadSafeCache::<String, i32>::new();
let mut handles = vec![];
// Writer threads
for i in 0..5 {
let cache_clone = cache.clone();
let handle = thread::spawn(move || {
cache_clone.insert(format!("key_{}", i), i * 10);
thread::sleep(Duration::from_millis(10));
});
handles.push(handle);
}
// Reader threads
for i in 0..5 {
let cache_clone = cache.clone();
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(5));
if let Some(value) = cache_clone.get(&format!("key_{}", i)) {
println!("Read key_{}: {}", i, value);
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final cache size: {}", cache.len());
}
Send is a marker trait with no runtime overheadMutex and RwLock have runtime overheadRwLock are faster than writesArc> : Simple but can cause contentionArc> : Better for read-heavy workloadsVec field. Is it Send? Is it Sync? Why?Rc not Send or Sync? What would you use instead?String into itArc> SendSend + 'static closuresSend + Sync boundsSend/Sync inference// Tokio requires Send for futures that are spawned
use tokio::runtime::Runtime;
let rt = Runtime::new().unwrap();
rt.spawn(async {
// This closure must be Send
println!("Running in Tokio task");
});
use rayon::prelude::*;
// Elements must be Send to parallelize
let sum: i32 = (0..1000).into_par_iter().sum();
Mutex: Requires T: Send to be SyncRwLock: Same requirements as MutexArc: Requires T: Send + Sync to be Send + Syncuse crossbeam::channel;
// Channel works with any Send type
let (tx, rx) = channel::unbounded();
Run this code in the official Rust Playground