Concurrency Fundamentals

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:

  1. Thread A gives up ownership and can no longer access val.
  2. Thread B now accesses, mutates, and eventually drops (Drop::drop) val on 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 closure f are moved from the spawning thread into the newly created thread.
  • T: Send: The return value T produced 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]