The Send Trait
std::marker::Send is an unsafe auto-trait (pub unsafe auto trait Send {}) indicating that ownership of a value of type T can be safely transferred from one thread to another. When a thread spawns with a move closure (std::thread::spawn(move || ...)), or when a value is sent over a cross-thread channel (mpsc::Sender<T>::send), the compiler requires T: Send.
What Moving a Value to Another Thread Entails
When Thread A creates a value val: T and moves it to Thread B:
- Thread A gives up ownership and can no longer access
val. - Thread B now accesses, mutates, and eventually drops (
Drop::drop)valon Thread B.
Therefore, a type T is safe to implement Send if and only if accessing, mutating, and running the destructor of T on a different thread from the thread that created it cannot violate memory safety or OS thread-affinity invariants.
Thread A (Creator) Thread B (New Owner)
+---------------------------+ +---------------------------+
| let buf = Vec::from(...); | | |
| tx.send(buf).unwrap(); ---+--[ T: Send ]-> let buf = rx.recv()...; |
| // `buf` moved out! | | buf.push(4); |
+---------------------------+ | // `buf` dropped here! |
+---------------------------+
Why Specific Types Are !Send
Almost all types in Rust implement Send automatically. The exceptions fall into three clear categories:
Non-Send Type (!Send) |
Why Transferring Ownership Across Threads Is Unsound |
|---|---|
Rc<T> and rc::Weak<T> |
Moving a cloned Rc<T> to Thread B while Thread A keeps the original Rc<T> would cause both threads to increment/decrement the same non-atomic Cell<usize> reference counter concurrently! |
MutexGuard<'_, T> and RwLock*Guard<'_, T> |
Operating-system mutexes (such as POSIX pthread_mutex_t and Windows critical sections) require the lock to be unlocked by the exact same OS thread that acquired it; dropping a MutexGuard on another thread is undefined behavior in OS threading libraries! |
Raw pointers (*const T, *mut T) |
Marked !Send as a lint/safety speedbump so structs containing raw pointers must explicitly audit and declare unsafe impl Send rather than becoming Send accidentally. |
How Send Bounds Are Used in std::thread::spawn
The signature of std::thread::spawn requires both the closure F and its return value T to implement Send:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
F: Send: All values captured inside the closurefare moved from the spawning thread into the newly created thread.T: Send: The return valueTproduced by the spawned thread is moved back to the parent thread when.join()is called!
use std::cell::RefCell;
use std::thread;
fn main() {
// `RefCell<Vec<i32>>` IS `Send` (even though it is `!Sync`!), because
// moving a uniquely owned `RefCell` to another thread leaves zero aliases behind:
let unique_cell = RefCell::new(vec![1, 2, 3]);
let handle = thread::spawn(move || {
unique_cell.borrow_mut().push(4);
unique_cell
});
let returned_cell = handle.join().unwrap();
println!("returned RefCell from worker thread: {:?}", returned_cell.borrow());
}
returned RefCell from worker thread: [1, 2, 3, 4]