ExamplesBy LevelBy TopicLearning Paths
441 Fundamental

441: Thread Basics — Spawn and Join

Functional Programming

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

  • • Understand how thread::spawn creates OS threads with move closures
  • • Learn how JoinHandle::join() waits for thread completion and propagates panics
  • • See how T: Send + 'static bounds enforce safe thread-boundary crossing
  • • Understand the parallel_compute pattern: map work items to threads, collect results
  • • Learn how thread panics are handled via Result<T, Box<dyn Any + Send>>
  • Code Example

    let handle = thread::spawn(move || {
        let r = 42 * 42;
        println!("Result: {}", r);
        r
    });

    Key Differences

  • 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.
  • Type safety: Rust's Send + 'static bounds prevent data races at compile time; OCaml threads can share any value without type-system enforcement.
  • Panic handling: Rust's JoinHandle::join() returns Result — panics are caught and returned; OCaml's Thread.create propagates exceptions differently.
  • Performance: Rust OS threads match C pthreads in performance; OCaml 5.x domains are slightly heavier than pthreads.
  • 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    FeatureOCamlRust
    Spawn syntaxThread.create f argthread::spawn(move \|\| ...)
    Return valueunit onlyAny Send + 'static type
    Join resultunitResult<T, Box<dyn Any>>
    Panic handlingCrashes domainErr returned to joiner
    Data captureGC managedmove closure with ownership
    Thread safetyRuntime (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

  • Parallel sort: Implement parallel merge sort using 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.
  • Thread pool manual: Without using a crate, build a simple ThreadPool with N threads that processes jobs from a Arc<Mutex<VecDeque<Box<dyn FnOnce() + Send>>>>. Verify it processes all jobs.
  • Panic recovery: Spawn 10 threads where some randomly panic. Use JoinHandle::join() to collect both successful results and panics, returning a Vec<Result<T, String>> where errors show the panic message.
  • Open Source Repos