Creating Threads with thread.spawn
std::thread::spawn creates a new native operating-system thread that executes a closure F concurrently with the calling thread and returns a JoinHandle<T> representing the spawned thread's lifecycle and return value.
Function Signature and Trait Bounds
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
| Bound | Requirement | Why It Is Required |
|---|---|---|
F: FnOnce() -> T |
Closure is called once and returns T |
Each spawned thread executes its entry point once from start to finish |
F: Send |
Closure environment can cross threads | All captured variables are transferred from the parent thread to the child thread |
F: 'static |
Closure holds no non-'static references |
A detached thread can outlive the stack frame that spawned it |
T: Send + 'static |
Return value can be sent back across threads | .join() moves T from the child thread back to the joining thread |
Parent Thread (main) OS Kernel / Child Thread
+-----------------------------------+ +-----------------------------------+
| let handle = thread::spawn(|| { +----> | Allocates 2 MiB stack frame |
| compute_work() | | Runs `compute_work()` concurrently|
| }); | | Stores return value `T` |
| // Continues executing in parallel| +-----------------+-----------------+
| let res = handle.join().unwrap(); | <----------------------+
+-----------------------------------+
Spawning Threads with thread::spawn vs. thread::Builder
While thread::spawn(f) uses the default stack size (2 MiB, or the RUST_MIN_STACK environment variable) and an unnamed thread, std::thread::Builder customizes the thread's human-readable name (shown in OS debuggers, top/htop, and panic messages), stack size, and returns an io::Result<JoinHandle<T>> instead of panicking if the OS refuses to create a thread:
| Spawning Method | Custom Thread Name? | Custom Stack Size? | Behavior on OS Thread Creation Failure |
|---|---|---|---|
thread::spawn(f) |
No (<unnamed>) |
Default (2 MiB) |
Panics (failed to spawn thread) |
thread::Builder::new().spawn(f) |
Yes (.name("worker-1".into())) |
Yes (.stack_size(4 * 1024 * 1024)) |
Returns Err(std::io::Error) |
use std::thread;
fn main() {
// 1. Standard `thread::spawn`:
let default_handle = thread::spawn(|| {
format!(
"unnamed thread (name = {:?})",
thread::current().name()
)
});
// 2. Configured `thread::Builder` with custom name and 4 MiB stack:
let named_handle = thread::Builder::new()
.name(String::from("indexer-worker"))
.stack_size(4 * 1024 * 1024)
.spawn(|| {
format!(
"named thread (name = {:?})",
thread::current().name()
)
})
.expect("OS failed to spawn thread");
println!("{}", default_handle.join().unwrap());
println!("{}", named_handle.join().unwrap());
}
unnamed thread (name = None)
named thread (name = Some("indexer-worker"))
What Happens When main Exits Before Spawned Threads Finish
When the main thread of a Rust program finishes fn main(), the operating system terminates the entire process immediately—stopping any still-running background threads mid-execution without unwinding their stacks. Always call .join() on JoinHandle<T> when you need a background thread's work and destructors to complete before exiting main.