Skip to content

← Fearless Concurrency: Threads, Channels, Shared State step 1 of 24

Easy Primitives

Threads, spawn and join

Spawn n threads. Thread i computes i * i. Return the squares in index order.

pub fn spawn_squares(n: usize) -> Vec<u64>

n = 0 gives [], n = 8 gives [0, 1, 4, 9, 16, 25, 36, 49]. Assume n <= 256 — the grader runs on your own machine and spawning thousands of OS threads is antisocial.

Threads in Rust are detached by default

std::thread::spawn starts an OS thread and returns a JoinHandle<T>. There is no parent/child relationship. Nothing links the new thread’s lifetime to the one that started it, and when main returns the process exits, killing every thread still running. That is the single most common first-day bug: a program that spawns work, prints nothing, and exits successfully.

The fix is join:

let handle = std::thread::spawn(|| 6 * 7);
let answer: i32 = handle.join().unwrap();   // blocks until the thread ends

Two things to notice in that one line. join blocks the calling thread until the spawned one finishes. And it returns the closure’s value — a thread is not a void fire-and-forget in Rust, it is a computation that hands you a result. join gives you a std::thread::Result<T>; unwrap is fine here because our closure cannot panic, and the next problem covers what that Result is actually for.

The compiler will not warn you

Worth knowing before you start: dropping a JoinHandle without joining it is not a warning. Neither rustc nor clippy -D warnings says a word about

thread::spawn(move || expensive());   // silently detached

It is legal — detaching is sometimes what you want. So this is a class of bug the toolchain does not catch for you, which is a useful early calibration on what “fearless concurrency” does and does not promise. Rust eliminates data races. It does not eliminate forgetting to wait.

Why the shape of this problem matters

Threads finish in whatever order the OS scheduler feels like. If you push results from inside the threads into some shared collection, the order of that collection is not yours to predict, and a test asserting [0, 1, 4, 9] would fail at random.

The structure here avoids that entirely:

  1. spawn all n threads, collecting the handles into a Vec in index order;
  2. then join them, in that same order, collecting the values.

Join order is your choice, not the scheduler’s, so the output is deterministic. Joining handle 3 first just means you wait a bit longer at that line — every thread still ran concurrently. Collect in join order, not in completion order is the trick this entire track is built on; you will see it again as index tagging.

Note the two-phase shape is load-bearing. If you spawn and join inside one loop you have written a sequential program with extra steps — each thread finishes before the next one starts.

Finally: println! from inside a thread will not help you here. The grader reads the value your function returns, and stdout from a worker is not it. The answer has to come back through join().

Loading visualization…