RustArena

Step 5 of 7

Send and Sync: what may cross threads

How does Rust decide what a thread may touch? With two marker traits: traits with no methods that only describe a type.

You almost never write these yourself. They're auto traits: the compiler implements them for your struct or enum automatically when all of its fields have them. Numbers and String are Send and Sync; so are Vec<T>, Box<T> and HashMap when what they hold is, and so are your own structs built from them.

thread::spawn asks for them in its signature (simplified):

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static,

Read it as: the closure runs once (FnOnce), the closure and its result must be Send (they change threads), and both must be 'static (no borrowed locals, step 2).

Rc is not Send

Rc<T> (module 29) keeps its reference count in a plain integer. If two threads cloned the same Rc at the same moment, both could read the count 1 and both write 2: one increment is lost, the data is freed while someone still uses it, and you have a use-after-free. So Rc is neither Send nor Sync, and the compiler refuses:

use std::rc::Rc;
use std::thread;

fn main() {
    let names = Rc::new(vec![String::from("ana")]);
    let copy = Rc::clone(&names);
    let handle = thread::spawn(move || copy.len());
    println!("{}", handle.join().unwrap());
}
error[E0277]: `Rc<Vec<String>>` cannot be sent between threads safely

The fix is Arc<T> (std::sync::Arc, "atomically reference counted"), Rc's thread-safe twin. Same API (Arc::new, Arc::clone), but the count is updated with atomic CPU instructions that can't lose updates, which costs a little more. The next module covers Arc in depth; for now, the swap is all you need:

use std::sync::Arc;
use std::thread;

fn main() {
    let menu = Arc::new(vec![String::from("kelp soup"), String::from("crab cakes")]);
    let for_worker = Arc::clone(&menu);
    let handle = thread::spawn(move || for_worker.len());
    println!("worker sees {} dishes", handle.join().unwrap());
    println!("main still has {:?}", menu);
}
worker sees 2 dishes
main still has ["kelp soup", "crab cakes"]

Likewise, Cell and RefCell are Send but not Sync: their borrow tracking isn't thread-safe, so sharing a &RefCell between threads fails with error[E0277]: `RefCell<i32>` cannot be shared between threads safely. Their thread-safe counterparts are Mutex and RwLock, in the next module.

Data races are compile errors

A data race happens when two threads access the same memory at the same time, at least one of them writes, and nothing coordinates them. In C and C++ that's undefined behavior: corrupted totals that show up once a week in production. In Rust, the borrowing rules you already know rule it out, because &mut means exclusive across threads too:

use std::thread;

fn main() {
    let mut total = 0;
    thread::scope(|s| {
        s.spawn(|| total += 1);
        s.spawn(|| total += 2);
    });
    println!("{total}");
}
error[E0499]: cannot borrow `total` as mutable more than once at a time

This is what Rust calls fearless concurrency: if your code compiles (without unsafe), it has no data races. It can still have other bugs, like deadlocks or wrong logic, but not that one. The fixes are to give each thread its own data and combine the results afterwards (as in every exercise so far), or to use a synchronization type like Mutex or an atomic (next module).

Common mistakes

Your task

This program shares a list of banned words with a checker thread using Rc, and doesn't compile. Switch it to Arc so it prints:

flagged: 2
banned words: 3

Challenge 2

Two scoped threads both push into report, so the program doesn't compile (E0499). Make each thread return its line instead, join them in order (big first), and build report from the results in the main thread. Don't use any locks. Output:

big: 3
small: 3

Challenge 3

Write a generic fn run_in_thread<T, R, F>(input: T, f: F) -> R that runs f(input) on a new thread (with thread::spawn), waits for it, and returns the result. The starter only says that f is a function taking a T and returning an R; the compiler will tell you which extra bounds T, R and F need. They're the same ones thread::spawn asks for.

Previous: Scoped threads: borrowing local data Next: When a thread panics