Skip to content

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

Easy Primitives

Threads, spawn and join

Write a function that starts n threads. Thread i (for i from 0 to n - 1) computes i * i as a u64 and hands it back. Return the squares in index order.

pub fn spawn_squares(n: usize) -> Vec<u64>
  • The result has exactly n elements, and element i is i * i.
  • n = 0 returns an empty vector, [].
  • n = 1 returns [0].
  • n = 8 returns [0, 1, 4, 9, 16, 25, 36, 49].
  • The tests use n <= 256, so every square fits easily in a u64. The grader runs your code on your own machine, and the limit keeps the number of operating-system threads small.

Compute each square inside the thread started for it. The exercise is about getting a value out of a thread and back to the caller.

Starting a thread and waiting for it

std::thread::spawn takes a closure, starts a new operating-system thread that runs it, and immediately returns a JoinHandle<T>, where T is the closure's return type. The calling thread does not wait; both threads now run concurrently.

A handle is how you collect the result:

use std::thread;

let handle = thread::spawn(|| 6 * 7);
let answer: i32 = handle.join().unwrap(); // waits for the thread to finish
assert_eq!(answer, 42);

join does two things. It blocks: the calling thread stops at that line until the spawned thread has finished. And it returns the closure's value, wrapped in a std::thread::Result<T>. That Result is Ok(value) when the closure returned normally and Err(...) when the thread panicked. Squaring a number below 256 cannot panic, so unwrap or expect is reasonable here.

Dropping a handle detaches the thread

If you drop a JoinHandle without calling join, the thread is detached: it keeps running, but nothing can wait for it or read its result any more. Detaching is legal and sometimes intended, so the default compiler lints and the default Clippy lints do not report it. For example, this compiles without a warning:

thread::spawn(move || expensive(input)); // handle dropped: detached

Detached threads have a second consequence. When the program's main thread returns from main, the whole process exits, and any threads still running are stopped without finishing. A program that spawns work and forgets to join it can therefore exit before that work has happened, without any error.

Rust's type system prevents data races in safe code, but it does not check that you waited for a thread whose result you need. The starter code has exactly this problem: it spawns every thread, drops every handle, and returns an empty vector.

Getting a deterministic order

The operating system decides when each thread runs, so threads can finish in any order, and that order can change from run to run. If each thread pushed its result into a shared collection, the order of that collection would vary, and a test expecting [0, 1, 4, 9] could fail on some runs.

Joining avoids that problem when you do it in two phases:

  1. spawn all n threads, storing the handles in a Vec in index order;
  2. then go through that Vec from front to back, calling join on each handle and collecting the values.

The output order is now the order you call join, which you control, not the order in which threads finish. If thread 3 finishes before thread 0, its value simply waits in its handle until you reach it.

Keep the two phases separate. If one loop spawns a thread and immediately joins it, each thread finishes before the next one starts, so no two threads ever run at the same time. The output would be correct, but the threads would add cost without allowing any concurrency.

Printing from inside a thread does not count as returning a value: the grader reads only the vector your function returns, so each square has to come back through join.