Send and Sync Traits
Recipe | Crates | Categories |
---|---|---|
Existing Implementations of Send and Sync | ||
Send Trait | ||
Sync Trait | ||
Using Arc and Mutex for Safe Concurrent Access to Shared Data | ||
Implementing Send and Sync |
The Send
and Sync
traits are fundamental to Rust's concurrency. You can think of Send
as "Exclusive access is thread-safe," and Sync
as "Shared access is thread-safe."
T
is Sync
if and only if &T
is Send
.
Existing Implementations of Send
and Sync
Traits | Types |
---|---|
Send and Sync | primitives; (T1, T2) , [T; N] , &[T] , struct { x: T } , Arc , Vec , Box , Option ⮳ (depending on underlying types); String ⮳, &str ; Mutex , Atomic* ... |
!Send and !Sync | Rc , raw pointers *const T ,*mut T , types from external libraries or the operating system that are not thread safe. |
Send and !Sync | mpsc::Receiver<T> ; UnsafeCell , Cell , RefCell : when a type has interior mutability, we must be sure that we mutate it from one place only, but this place can be everywhere as long as it is singular. |
!Send and Sync (rare) | RwLockReadGuard , RwWriteGuard and MutexGuard ⮳; &mut T if T is !Send ; structs which use thread-local storage and accesses that info in Drop . |
Send
Trait
A type is Send
if it can be transferred across thread boundaries. Most types in Rust are Send
by default, as long as they don't contain non-Send
types.
Send
allows an object to be used by two or more threads at different times. Thread 'A' can create and use an object, then send it to thread 'B', so thread 'B' can use the object while thread 'A' cannot. The Rust ownership model can be used to enforce this non-overlapping use. In other words, Send
means that a type is safe to move from one thread to another. If the same type also implements Copy
, it is safe to copy from one thread to another.
An important exception is Rc
. By cloning, it allows data to have multiple owners. If one owner in thread 'A' could send the Rc
to another thread, giving ownership to thread 'B', there could be other owners in thread 'A' that can still use the object. Since the reference count is modified non-atomically, the value of the count on the two threads may get out of sync and one thread may drop the pointed-at value while there are owners in the other thread. Therefore Rc
does not implement Send
.
Sync
Trait
A type is Sync
if it is safe to be referenced from multiple threads simultaneously. This is trivial for immutable objects, but mutations need to be synchronized (performed in sequence with the same order being seen by all threads). This is often done using a Mutex
or RwLock
which allows one thread to proceed while others must wait. By enforcing a shared order of changes, these types can turn a non-Sync
object into a Sync
object. Another mechanism for making objects Sync
is to use atomic types, which are essentially Sync
primitives.
Arc
is an Rc
that uses an atomic type for the reference count. Hence, it can be used by multiple threads without the count getting out of sync. If the data that the Arc
points to is Sync
, the entire object is Sync
. If the data is not Sync
(e.g. a mutable type), it can be made Sync
using a Mutex
. Hence the proliferation of Arc<Mutex<T>>
types in multi-threaded Rust code, as we will see below.
Using Arc
and Mutex
for Safe Concurrent Access to Shared Data
//! Demonstrates safe concurrent access to shared data //! using `Arc` (Atomic Reference Counting) and `Mutex` (Mutual Exclusion). //! //! - `Arc` allows multiple threads to have shared ownership of the data. //! - `Mutex` ensures that only one thread can access the data at a time. //! //! Under the covers, `Mutex<T>` is `Send + Sync` if `T` is `Send`. //! Note that `T: Sync` is not required, as the underlying data is only made //! available to one thread at a time. use std::sync::Arc; use std::sync::Mutex; use std::thread; fn main() { let data = Arc::new(Mutex::new(0)); let mut handles = vec![]; // Create 3 threads, each of which increments the shared data by 1. for _ in 0..3 { let data = Arc::clone(&data); let handle = thread::spawn(move || { let mut num = data.lock().unwrap(); *num += 1; }); handles.push(handle); } // Wait for all threads to finish. for handle in handles { handle.join().unwrap(); } println!("Result: {}", *data.lock().unwrap()); }
Implementing Send
and Sync
Send
and Sync
are automatically derived traits. This means that, unlike almost every other trait, if a type is composed entirely of Send
or Sync
types, then it is Send
or Sync
.
If you want to work with non-Sync
/ Send
types like raw pointers, you need to build an abstraction on which Send
and Sync
can be derived.
The following provides an example of such abstraction - a simplified implementation of the Arc
(Atomic Reference Counted) smart pointer, which, as discussed, can safely share data between multiple owners in a thread-safe manner. my::Arc
consists of an outer struct that wraps a raw pointer to an inner struct (ArcInner
), which contains the data and the reference count. Cloning Arc
creates another outer struct that points to the same inner struct and increase the reference count. Dropping an instance of Arc
decreases the reference count and, if zero, drops the underlying data.
By implementing the unsafe marker traits Send
and Sync
on my::Arc
, we guarantee that this struct can indeed be sent across threads safely. The usage of my::Arc
must not cause data races or other thread safety issues. An incorrect implementation can cause Undefined Behavior. Caveat lector!
//! Simplified implementation of the `Arc` smart pointer. //! //! Invoking `clone` on `Arc` produces a new `Arc` instance, which points to the //! same allocation on the heap as the source `Arc`, while increasing a //! reference count. //! //! This implementation is not intended for production use. #![allow(dead_code)] mod my { use std::marker::PhantomData; use std::marker::Send; use std::marker::Sync; use std::mem::ManuallyDrop; use std::ops::Deref; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering::Relaxed; use std::sync::atomic::Ordering::Release; /// Private inner data structure. /// It holds the actual data and the reference count. /// One instance is shared by / pointed to by multiple `Arc` clones. struct ArcInner<T> { count: AtomicUsize, data: T, } // `ArcInner` should be `Send` and `Sync` if the underlying data is. unsafe impl<T: Sync + Send> Send for ArcInner<T> {} unsafe impl<T: Sync + Send> Sync for ArcInner<T> {} /// A simplified implementation of `Arc`. /// The fields are private and unreachable outside of the module. pub struct Arc<T> { // Pointer to the inner data structure. ptr: *mut ArcInner<T>, // We want `Arc` to behave like a `ArcInner<T>`, despite the fact that // it does not own one, thus we use `PhantomData`. phantom: PhantomData<ArcInner<T>>, } // SAFETY: By implementing `Send` for `Arc`, we declare it can be sent // across threads safely. By implementing `Sync`, we declare it can be // shared across threads safely. unsafe impl<T: Sync + Send> Send for Arc<T> {} unsafe impl<T: Sync + Send> Sync for Arc<T> {} impl<T> Arc<T> { /// Creates a new `Arc` instance. /// /// # Arguments /// /// * `data` - The data to be shared. pub fn new(data: T) -> Self { // Create the inner data structure on the heap. // We also inhibit the compiler from automatically calling its // destructor using `ManuallyDrop`. let mut inner = ManuallyDrop::new(Box::new(ArcInner { count: AtomicUsize::new(1), data, })); // We store a reference to the data as a raw pointer // (which is `!Sync` and `!Send` by itself). let ptr: *mut ArcInner<T> = std::ptr::from_mut(&mut **inner); // A reference is just a pointer that is assumed to be aligned, not // null, and pointing to memory containing a valid value // of `T` in this case. Thus the pointer made from it // is as well. assert!(!ptr.is_null()); assert!(ptr.is_aligned()); println!("Created `Arc` with reference count 1."); Arc { ptr, phantom: PhantomData, } } /// Returns a reference to the inner data. /// /// SAFETY: this function dereferences a raw pointer. /// This said, `new()` guarantees that the inner pointer is valid. fn inner(&self) -> &ArcInner<T> { assert!(!self.ptr.is_null()); assert!(self.ptr.is_aligned()); unsafe { self.ptr.as_ref().unwrap() } } /// Creates a new `Arc` from a raw pointer to an `ArcInner`. /// /// SAFETY: This function is unsafe because it takes a raw pointer as /// input. The caller must ensure that the pointer is valid and /// points to a properly initialized `ArcInner` instance. unsafe fn from_inner(ptr: *mut ArcInner<T>) -> Self { assert!(!ptr.is_null()); assert!(ptr.is_aligned()); Self { ptr, phantom: PhantomData, } } } /// Implements the `Clone` trait for `Arc`. /// /// This increments the reference count. impl<T> Clone for Arc<T> { fn clone(&self) -> Self { // Increment the reference count. // `count` is atomic, so this is thread-safe. let previous_count = self.inner().count.fetch_add(1, Relaxed); println!( "Increased the reference count; previous value: {previous_count}" ); Self { ptr: self.ptr, phantom: PhantomData, } } } // Implement `Deref` to make `Arc` a smart pointer. impl<T> Deref for Arc<T> { type Target = T; fn deref(&self) -> &Self::Target { // Dereference the pointer and create a reference to the data. unsafe { &(*self.ptr).data } } } /// Implements the `Drop` trait for `Arc`. /// /// This decrements the reference count and deallocates the data when the /// count reaches zero. impl<T> Drop for Arc<T> { fn drop(&mut self) { // Subtracts from the current value, returning the previous value. // `count` is atomic, so this is thread-safe. let previous_count = self.inner().count.fetch_sub(1, Release); if previous_count != 1 { println!( "Decreased reference count; previous count: {previous_count}" ); return; } // When the reference count is zero, we drop the underlying data. unsafe { std::ptr::drop_in_place(&mut (*self.ptr).data) }; // You could also use `ManuallyDrop::drop(&mut (*self.ptr))`; println!("`Arc` data fully dropped!"); } } } /// A struct that prints when dropped, for demonstration purposes. #[derive(Debug)] struct Droppable(i32); impl Drop for Droppable { fn drop(&mut self) { println!("`Droppable` drop called - its value was {}", self.0); } } /// Demonstrate the usage of `my::Arc`. /// /// This function creates an `my::Arc` instance, spawns multiple threads that /// access the shared data, and then waits for all threads to finish. fn main() { let value = Droppable(42); let reference = &value; println!("The address of `value` is {reference:p}"); let my_arc = my::Arc::new(value); // Spawn multiple threads to demonstrate that `Arc` is `Send` and `Sync`. let handles: Vec<_> = (0..3) .map(|i| { // Clone the `Arc` (increasing the reference count), // move the clone into the thread. let my_clone = my_arc.clone(); std::thread::spawn(move || { println!("Data: {:?}", *my_clone); assert_eq!(my_clone.0, 42); println!("`my_clone` in thread {i} will drop next.") }) }) .collect(); for handle in handles { handle.join().unwrap(); } // At this point, all clones have been dropped and only `my_arc` remains. println!("`my_arc` will drop next."); }