Rust's approach to concurrency is unique, relying heavily on its ownership system and type system to prevent data races and other common concurrency bugs at compile time. The `Sync` and `Send` traits are fundamental pillars of this approach, enabling safe and extensible concurrency.
The Send Trait
The `Send` trait indicates that a type can be safely transferred (moved) from one thread to another. If a type `T` implements `Send`, it means ownership of a value of type `T` can be safely transferred across thread boundaries. Most primitive types (like `i32`, `bool`, `char`), and types composed entirely of `Send` types (like `Vec<T>` where `T` is `Send`, or `Box<T>` where `T` is `Send`), automatically implement `Send`.
Key characteristics of `Send`:
* Auto-implemented: For most types that are safe to move across threads.
* Default: Many standard library types are `Send` by default.
* Not `Send` examples: Raw pointers (`*const T`, `*mut T`) are not `Send` because transferring them across threads without proper synchronization could lead to data races or memory unsafety. `Rc<T>` is also not `Send` because its reference count updates are not atomic, making it unsafe to share across threads.
* Requirement for `thread::spawn` closures: The arguments captured by a closure passed to `std::thread::spawn` must be `Send` so that the closure (and the data it owns) can be moved to the new thread.
The Sync Trait
The `Sync` trait indicates that a type can be safely shared between multiple threads by reference. If a type `T` implements `Sync`, it means that `&T` (an immutable reference to `T`) is `Send`. In other words, if you have a shared reference to a `Sync` type, you can send that reference to another thread, and both threads can safely access the data concurrently.
Key characteristics of `Sync`:
* Auto-implemented: For most types that are safe to share by reference across threads.
* Default: Many standard library types are `Sync` by default.
* Not `Sync` examples: `RefCell<T>` and `UnsafeCell<T>` are not `Sync` because they allow interior mutability without enforcing thread-safety. If multiple threads were to simultaneously access and mutate a `RefCell` through shared references, it would lead to data races. `Mutex<T>` *is* `Sync` (even though it contains `UnsafeCell<T>`), because it provides a mechanism (locking) to ensure only one thread can access the inner data at a time, making it safe for shared mutable access across threads.
* Relationship to `Send`: A type `T` is `Sync` if and only if `&T` is `Send`. This is an important distinction: `Send` is about moving ownership, `Sync` is about sharing references.
Together: Extensible Concurrency
Rust's `Send` and `Sync` traits are `auto-traits`, meaning the compiler automatically implements them for composite types if all their components implement them. This makes concurrency 'extensible' because most of the time, you don't need to manually implement these traits. As long as you compose your data structures from `Send` and `Sync` primitives (or types that internally use thread-safe mechanisms like `Mutex` or `RwLock`), your types will automatically inherit these properties.
When working with shared mutable state across threads, you often combine `Sync` with `Send`. For instance, `Arc<T>` (Atomic Reference Counted) allows multiple threads to own a pointer to the same data, and `Arc<T>` is `Send` if `T` is `Send`. If you want to share *mutable* data, you typically wrap it in `Arc<Mutex<T>>`. Here:
1. `T` needs to be `Send` so it can be moved into the `Mutex`.
2. `Mutex<T>` needs to be `Sync` so references to it can be shared across threads (allowing multiple threads to try to lock it).
3. `Arc<Mutex<T>>` needs to be `Send` so it can be moved into closures for `thread::spawn`.
This robust system allows Rust to provide strong compile-time guarantees against data races without requiring a garbage collector, making it a powerful choice for concurrent programming.
Example Code
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
fn main() {
// Example of Send: i32 is Send, so it can be moved into a new thread's scope.
let my_number = 10;
let handle1 = thread::spawn(move || {
println!("Thread 1 received: {}", my_number);
// my_number is moved into this closure, so the main thread no longer owns it.
});
handle1.join().unwrap();
// println!("Main thread still has my_number: {}", my_number); // This would be a compile error if my_number wasn't copied/cloned, as it was moved.
// Example of Sync: Using Arc<Mutex<T>> to share mutable state safely.
// Arc makes the Mutex<i32> itself reference-countable and Send.
// Mutex<i32> makes the i32 safely shareable and mutable across threads (i.e., it's Sync).
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for i in 0..5 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// Each thread gets its own Arc clone, which is Send.
// It then locks the Mutex, which is Sync.
let mut num = counter_clone.lock().unwrap();
*num += 1;
println!("Thread {} incremented counter to {}", i, *num);
thread::sleep(Duration::from_millis(50)); // Simulate some work
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final counter value: {}", *counter.lock().unwrap());
// What if we tried to send something non-Send?
// This would result in a compile-time error:
// use std::rc::Rc;
// let rc_value = Rc::new(5);
// let handle_rc = thread::spawn(move || {
// // error[E0277]: `Rc<i32>` cannot be sent between threads safely
// println!("Thread trying to use Rc: {}", *rc_value);
// });
// handle_rc.join().unwrap();
}








Extensible Concurrency with the Sync and Send Traits