🎓 A1Learner
🔍

Rust Threads

A thread lets a program do several things at once, like several cooks working in one kitchen. The compiler checks that threads cannot clash over data.

Start a thread with spawn

spawn takes a move closure — the thread owns the data it uses — and hands back a handle:

let words = vec!["rust", "is", "fast"];
let handle = thread::spawn(move || words.len());

join waits and collects the result

join() pauses until the thread finishes, then hands back what the thread returned:

let n = handle.join().unwrap();
println!("thread counted {n} words");

If the thread panics, join returns Err

join does not crash with the thread. It reports the panic as an Err value:

let handle = thread::spawn(|| panic!("oh no"));
println!("join ok = {}", handle.join().is_ok());

The error when move is forgotten

spawn cannot prove that this closure finishes before name is dropped, so the borrow is refused. Adding move is the fix the compiler will suggest:

let name = String::from("main");
let handle = thread::spawn(|| println!("{name}"));
// error E0373: closure may outlive the current function

Scoped threads may borrow

thread::scope guarantees every thread is finished before the scope ends. A thread may borrow local data — no move, no 'static:

let words = vec!["rust", "is", "fast"];
thread::scope(|s| {
    s.spawn(|| println!("borrowed {} words", words.len()));
});

Example

use std::thread;
fn main() {
    let words = vec!["rust", "is", "fast"];
    let handle = thread::spawn(move || words.len());
    let n = handle.join().expect("thread panicked");
    println!("thread counted {} words", n);
}

Key points

  • A thread is a separate path through the program: several threads can run at the same time.
  • When the main thread ends, all spawned threads are shut down at once — finished or not.
  • A thread you never join may run partly, or not at all.
  • Where you call join matters.
  • Calling it straight after spawn makes main wait before doing its own work.
  • The two threads stop overlapping.
  • Operating-system threads are heavy.
  • Each reserves about a megabyte of stack.
  • Use a pool or async for thousands of tasks.
  • spawn demands F: Send + 'static, and a closure is Send exactly when everything it captures is Send.
  • Capture an Rc and the closure stops being Send — error E0277, Rc<String> cannot be sent between threads safely.
  • Arc is the fix, and it appears later in this section.
  • Use thread::Builder::new().name("uploader") to name a thread when you spawn it.
  • The name appears in panic messages and in debuggers.
  • That beats an anonymous ThreadId when a program misbehaves.
  • Data that changes needs Arc<Mutex<T>> — both appear later in this section.
  • The golden rule still holds, with many readers or one writer.

In this example, words is a Vec of three words. thread::spawn starts a new thread with a move closure, so words moves into the thread. The thread counts the words and returns 3 as its result. Meanwhile main holds the handle. handle.join() waits for the thread to finish and hands the 3 back. If the thread had panicked, join would return an Err. expect would stop the program with the message "thread panicked". The println! shows thread counted 3 words.

Note

Data that is only read can be shared with Arc<T>.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why do spawned threads usually need move closures?