22.4 Creating Threads in Rust

Rust gives you direct access to OS threading via std::thread. Each thread has its own stack and is scheduled preemptively by the OS. If you’re familiar with POSIX threads or C++ <thread>, Rust’s APIs will feel similar but with added safety from the ownership model.

22.4.1 std::thread::spawn

Use std::thread::spawn to create a new thread, which takes a closure or function and returns a JoinHandle<T>:

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("Hello from spawned thread {i}!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    thread::sleep(Duration::from_millis(5));
    println!("Hello from the main thread!");

    // Wait for the spawned thread to finish.
    handle.join().expect("The thread being joined has panicked");
}

Key details:

  • The new thread runs concurrently with main.
  • thread::sleep mimics blocking work, causing interleaving of outputs.
  • join() makes the main thread wait for the spawned thread to complete.

A JoinHandle<T> can return a value:

use std::thread;

fn main() {
    let arg = 100;
    let handle = thread::spawn(move || {
        let mut sum = 0;
        for j in 1..=arg {
            sum += j;
        }
        sum
    });

    let result = handle.join().expect("Thread panicked");
    println!("Sum of 1..=100 is {result}");
}

To share data across threads, you can move ownership into the thread or use safe concurrency primitives like Arc<Mutex<T>>. Rust prevents data races at compile time, rejecting code that attempts unsynchronized sharing.

Tip: Spawning many short-lived threads can be expensive. A thread pool (e.g., in Rayon or a dedicated crate) often outperforms spawning threads repeatedly.

22.4.2 Thread Names and the Builder Pattern

For more control over thread creation (e.g., naming threads or adjusting stack size), use std::thread::Builder:

use std::thread;
use std::time::Duration;

fn main() {
    let builder = thread::Builder::new()
        .name("worker-thread".into())
        .stack_size(4 * 1024 * 1024); // 4 MB

    let handle = builder.spawn(|| {
        println!("Thread {:?} started", thread::current().name());
        thread::sleep(Duration::from_millis(100));
        println!("Thread {:?} finished", thread::current().name());
    }).expect("Failed to spawn thread");

    handle.join().expect("Thread panicked");
}

Naming threads helps with debugging, as some tools display thread names. If you rely on deep recursion or large stack allocations, you may need to increase the default stack size—but do so carefully to avoid unnecessary memory usage.