🎓 A1Learner
🔍

Rust Send & Sync

Send means a value may travel to another thread, and Sync means it may be shared by reference between threads. Most types earn both labels automatically.

Send: a value may travel to another thread

Send and Sync are built into the language itself. Almost every other concurrency tool lives in the standard library or in crates. A type is Sync when sharing &T between threads cannot cause a data race:

use std::thread;
fn main() {
    let word = String::from("movable");
    let h = thread::spawn(move || word.len());
    println!("String is Send: len = {}", h.join().unwrap());
}

Rc has neither label — the compiler refuses

Rc is neither Send nor Sync. Arc is both. That is the whole reason Arc exists. RefCell is not Sync. Its borrow checks are not thread-safe. Mutex is Sync:

use std::rc::Rc;
use std::thread;
fn main() {
    let rc = Rc::new(5);
    thread::spawn(move || println!("{}", rc));
    // error E0277: Rc<i32> cannot be sent between threads safely
}

The labels are worked out from the parts

A type is Send when all its parts are Send — Rust works this out for you. Mutex<T> is Send and Sync exactly when T is Send. The lock makes sharing safe. But a value that itself is not Send still cannot move between threads:

fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn main() {
    assert_send::<std::sync::Arc<String>>();
    assert_sync::<std::sync::Mutex<i32>>();
    println!("Arc and Mutex carry both labels");
}

A data race is not a race condition

A data race is undefined behaviour — and impossible in safe Rust. A race condition can make a program wrong, but never memory-unsafe: Rust prevents data races, not race conditions:

// data race, exactly: two or more threads touch the same memory
// at the same time, at least one of them writes, and at least
// one access is unsynchronised
// race condition: the result depends on the order threads happen
// to run — a thread checks a value, then acts after another
// thread changed it

Happens-before: the receipt that writes are visible

These labels have a companion in the atomics lessons of this section. Send and Sync decide whether sharing is allowed at all. A memory Ordering decides something else. It decides what a thread is guaranteed to see when a shared atomic changes. A happens-before edge is the receipt that one thread's writes are visible to another. Three everyday tools each mint one:

// a Mutex unlock, followed by a lock of the same Mutex
// a channel send, followed by the matching recv
// a Release store, followed by an Acquire load on the same atomic

Example

use std::sync::Arc; use std::thread;
fn assert_send<T: Send>(_: &T) {}
fn main() {
    let shared = Arc::new(String::from("thread-safe"));
    assert_send(&shared);
    let copy = Arc::clone(&shared);
    let h = thread::spawn(move || copy.len());
    println!("len across thread = {}", h.join().unwrap());
    // let rc = std::rc::Rc::new(1); assert_send(&rc); // Rc is Send, but sharing &Rc across threads via scope aside, Rc<RefCell> patterns stay single-threaded — Arc is the tool here.
}

Key points

  • Send and Sync are marker traits: they have no methods, they are pure promises checked at compile time
  • When you write unsafe impl Send for your own type, you make a promise.
  • The compiler cannot check that promise.
  • Treat it with care.
  • Raw pointers are the exception among basic types.
  • The compiler cannot check what they point at.
  • So they do not get the usual automatic Send and Sync treatment.
  • The compiler names the exact non-thread-safe part — it is the best debugging hint in this whole section.
  • Send covers travelling values, and Sync covers values shared by reference.

In this example, assert_send is a small test helper: it compiles only if the type given to it is Send. Arc<String> passes the test, as the assert_send line shows. A clone of the Arc then moves into a spawned thread, which measures the text's length: 11 characters in "thread-safe". join brings the 11 back, and the println! prints it. The commented-out lines at the bottom make the contrast. An Rc would fail these checks. Its count is not safe to update from several threads.

Note

When a thread error confuses you, read which field lost the label.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why can Rc not be sent between threads?