← thecodex.expert · The Codex Family of Knowledge
Rust

Concurrency and Threads

Rust calls this “fearless concurrency” — the same ownership rules that prevent memory bugs also catch most data races at compile time, before the program ever runs.

Rust 1.95+ Fearless concurrency Last verified:
Where this bites you

thread::spawn hands you a JoinHandle — and nothing forces you to use it. A spawned thread doesn't keep the program alive: when main returns, the process exits and every other thread is killed mid-work. Forget the join and the program compiles cleanly, exits with status 0, and the work you thought you had started simply never finishes.

Rustjoin_bug.rs
use std::thread;
use std::time::Duration;

thread::spawn(|| {
    for i in 1..4 {
        thread::sleep(Duration::from_millis(50));
        println!("worker: saved record {}", i);
    }
});

println!("main: done");
// output: main: done
// the worker's three "saved record" lines never print
Canonical Definition

std::thread::spawn creates a genuine OS thread running a closure concurrently with the caller. Rust's ownership rules extend naturally to threads: moving data into a thread with the move keyword transfers ownership, so the original thread can no longer access it, and Send/Sync marker traits let the compiler reject at compile time any type that isn't actually safe to share or transfer across threads. This is the basis of what the Rust community calls "fearless concurrency" — most data races are caught before the program runs, not discovered later by testing or in production.

Spawning a thread

thread::spawn returns a JoinHandle; calling .join() on it blocks the current thread until the spawned one finishes.

Rustthreads.rs
use std::thread;

let handle = thread::spawn(|| {
    for i in 1..5 {
        println!("spawned thread: {}", i);
    }
});

for i in 1..3 {
    println!("main thread: {}", i);
}

handle.join().unwrap();   // wait for the spawned thread to finish

That's why the example above ends with handle.join().unwrap(): it's the only thing that makes main wait. Keeping the JoinHandle and joining it before main returns lets the worker finish — and join() also hands back any panic from the thread as an Err, instead of losing it:

Rustjoin_fix.rs
use std::thread;
use std::time::Duration;

let handle = thread::spawn(|| {
    for i in 1..4 {
        thread::sleep(Duration::from_millis(50));
        println!("worker: saved record {}", i);
    }
});

handle.join().unwrap();
println!("main: done");
// worker: saved record 1
// worker: saved record 2
// worker: saved record 3
// main: done

The compiler enforces safe data sharing

A closure that captures a variable and is sent to a thread must use move, transferring ownership — without it, the compiler cannot guarantee the captured data outlives the thread, and refuses to compile.

Rustmove_thread.rs
let data = vec![1, 2, 3];

let handle = thread::spawn(move || {   // move required: ownership transfers to the thread
    println!("{:?}", data);
});

handle.join().unwrap();
// println!("{:?}", data);   // COMPILE ERROR: data was moved into the thread

Channels: message passing between threads

std::sync::mpsc ("multiple producer, single consumer") provides channels for threads to communicate by sending owned values, rather than sharing mutable memory directly.

Rustchannels.rs
use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    tx.send("hello from the thread").unwrap();
});

let received = rx.recv().unwrap();   // blocks until a message arrives
println!("{}", received);

Sources

1
The Rust Project. The Rust Programming Language, "Fearless Concurrency", doc.rust-lang.org/book/ch16.