441: Thread Basics — Spawn and Join
Tutorial Video
Text description (accessibility)
This video demonstrates the "441: Thread Basics — Spawn and Join" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Modern CPUs have multiple cores that sit idle when code runs single-threaded. Key difference from OCaml: 1. **GIL**: OCaml 4.x threads share a GIL preventing parallel execution; Rust threads run truly in parallel for both CPU and I/O bound work.
Tutorial
The Problem
Modern CPUs have multiple cores that sit idle when code runs single-threaded. std::thread::spawn creates OS threads that run truly in parallel on separate cores. Unlike async tasks (which are lightweight but still single-core unless you use an async runtime with a thread pool), OS threads run independently and can leverage all available cores for CPU-bound work. The challenge is safely sharing data between threads — Rust's type system enforces this at compile time via Send and Sync bounds.
Thread spawning is the foundation of parallel computing in Rust: used in build tools, data processing pipelines, game engines, scientific computing, and any CPU-bound workload.
🎯 Learning Outcomes
thread::spawn creates OS threads with move closuresJoinHandle::join() waits for thread completion and propagates panicsT: Send + 'static bounds enforce safe thread-boundary crossingResult<T, Box<dyn Any + Send>>Code Example
let handle = thread::spawn(move || {
let r = 42 * 42;
println!("Result: {}", r);
r
});Key Differences
Send + 'static bounds prevent data races at compile time; OCaml threads can share any value without type-system enforcement.JoinHandle::join() returns Result — panics are caught and returned; OCaml's Thread.create propagates exceptions differently.OCaml Approach
OCaml 4.x uses Thread.create f arg to spawn threads, but the GIL limits true parallelism to I/O-bound work. OCaml 5.x introduces Domain.spawn for true parallel domains without GIL. Thread.join t waits for completion. Unlike Rust, OCaml has no compile-time Send checking — any value can cross thread boundaries, and data races are possible in OCaml 5.x without explicit synchronization.
Full Source
#![allow(clippy::all)]
//! # Thread Basics — Spawn and Join
//!
//! Launch OS threads with `std::thread::spawn`, collect results with
//! `JoinHandle::join`, and catch panics without crashing the process.
use std::thread::{self, JoinHandle};
use std::time::Duration;
/// Approach 1: Spawn multiple threads and collect their results
///
/// Maps work items to threads and joins them to collect results.
pub fn parallel_compute<T, R, F>(items: Vec<T>, f: F) -> Vec<R>
where
T: Send + 'static,
R: Send + 'static,
F: Fn(T) -> R + Send + Sync + 'static + Clone,
{
let handles: Vec<JoinHandle<R>> = items
.into_iter()
.map(|item| {
let f = f.clone();
thread::spawn(move || f(item))
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("thread panicked"))
.collect()
}
/// Approach 2: Simple spawn and join pattern
///
/// Spawn a single thread with a computation and wait for the result.
pub fn spawn_and_join<T, F>(f: F) -> Result<T, Box<dyn std::any::Any + Send>>
where
T: Send + 'static,
F: FnOnce() -> T + Send + 'static,
{
thread::spawn(f).join()
}
/// Approach 3: Spawn with delay (simulating work)
///
/// Spawns threads with configurable delays to simulate varying workloads.
pub fn spawn_with_delays(count: usize, delay_ms: u64) -> Vec<usize> {
let handles: Vec<_> = (0..count)
.map(|i| {
thread::spawn(move || {
thread::sleep(Duration::from_millis(delay_ms * i as u64));
i * i
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
}
/// Check if a thread panic is safely contained
pub fn panic_contained() -> bool {
let handle = thread::spawn(|| -> i32 { panic!("intentional panic") });
handle.join().is_err()
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_spawn_and_join_success() {
let result = spawn_and_join(|| 42u32);
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_spawn_and_join_computation() {
let result = spawn_and_join(|| {
let mut sum = 0u64;
for i in 1..=100 {
sum += i;
}
sum
});
assert_eq!(result.unwrap(), 5050);
}
#[test]
fn test_multiple_threads() {
let handles: Vec<_> = (0..8u32).map(|i| thread::spawn(move || i * 2)).collect();
let results: Vec<u32> = handles.into_iter().map(|h| h.join().unwrap()).collect();
assert_eq!(results, vec![0, 2, 4, 6, 8, 10, 12, 14]);
}
#[test]
fn test_panic_is_caught() {
let handle = thread::spawn(|| panic!("boom"));
assert!(handle.join().is_err());
}
#[test]
fn test_panic_contained_helper() {
assert!(panic_contained());
}
#[test]
fn test_spawn_with_delays() {
let results = spawn_with_delays(4, 1);
assert_eq!(results, vec![0, 1, 4, 9]);
}
#[test]
fn test_thread_returns_string() {
let result = spawn_and_join(|| String::from("hello from thread"));
assert_eq!(result.unwrap(), "hello from thread");
}
#[test]
fn test_thread_captures_value() {
let value = 100;
let result = thread::spawn(move || value * 2).join().unwrap();
assert_eq!(result, 200);
}
}#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_spawn_and_join_success() {
let result = spawn_and_join(|| 42u32);
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_spawn_and_join_computation() {
let result = spawn_and_join(|| {
let mut sum = 0u64;
for i in 1..=100 {
sum += i;
}
sum
});
assert_eq!(result.unwrap(), 5050);
}
#[test]
fn test_multiple_threads() {
let handles: Vec<_> = (0..8u32).map(|i| thread::spawn(move || i * 2)).collect();
let results: Vec<u32> = handles.into_iter().map(|h| h.join().unwrap()).collect();
assert_eq!(results, vec![0, 2, 4, 6, 8, 10, 12, 14]);
}
#[test]
fn test_panic_is_caught() {
let handle = thread::spawn(|| panic!("boom"));
assert!(handle.join().is_err());
}
#[test]
fn test_panic_contained_helper() {
assert!(panic_contained());
}
#[test]
fn test_spawn_with_delays() {
let results = spawn_with_delays(4, 1);
assert_eq!(results, vec![0, 1, 4, 9]);
}
#[test]
fn test_thread_returns_string() {
let result = spawn_and_join(|| String::from("hello from thread"));
assert_eq!(result.unwrap(), "hello from thread");
}
#[test]
fn test_thread_captures_value() {
let value = 100;
let result = thread::spawn(move || value * 2).join().unwrap();
assert_eq!(result, 200);
}
}
Deep Comparison
OCaml vs Rust: Thread Basics
Spawning Threads
OCaml
let handle = Thread.create (fun () ->
let r = 42 * 42 in
Printf.printf "Result: %d\n%!" r;
r
) ()
Rust
let handle = thread::spawn(move || {
let r = 42 * 42;
println!("Result: {}", r);
r
});
Joining Threads
OCaml
(* Thread.join returns unit, cannot get return value *)
Thread.join handle
Rust
// JoinHandle::join returns Result<T, Box<dyn Any>>
let result: i32 = handle.join().unwrap();
Multiple Threads
OCaml
let handles = Array.init 4 (fun i ->
Thread.create (fun () -> i * i) ()
) in
Array.iter Thread.join handles
(* Cannot collect return values directly *)
Rust
let handles: Vec<_> = (0..4)
.map(|i| thread::spawn(move || i * i))
.collect();
let results: Vec<_> = handles
.into_iter()
.map(|h| h.join().unwrap())
.collect();
// results = [0, 1, 4, 9]
Key Differences
| Feature | OCaml | Rust |
|---|---|---|
| Spawn syntax | Thread.create f arg | thread::spawn(move \|\| ...) |
| Return value | unit only | Any Send + 'static type |
| Join result | unit | Result<T, Box<dyn Any>> |
| Panic handling | Crashes domain | Err returned to joiner |
| Data capture | GC managed | move closure with ownership |
| Thread safety | Runtime (GIL in some impls) | Compile-time (Send/Sync) |
Panic Safety
OCaml
(* Uncaught exception in thread propagates or crashes *)
let _ = Thread.create (fun () -> failwith "boom") ()
Rust
// Panic is contained, parent thread can handle it
let h = thread::spawn(|| panic!("boom"));
match h.join() {
Ok(v) => println!("got {}", v),
Err(_) => println!("thread panicked safely"),
}
Exercises
thread::spawn. Split the array in half, sort each half in a separate thread, then merge. Verify it produces the same result as sort() and benchmark the speedup on 10M elements.ThreadPool with N threads that processes jobs from a Arc<Mutex<VecDeque<Box<dyn FnOnce() + Send>>>>. Verify it processes all jobs.JoinHandle::join() to collect both successful results and panics, returning a Vec<Result<T, String>> where errors show the panic message.