Rust's std::thread::spawn: Create and Manage OS Threads
std::thread::spawn creates a new OS thread to run code concurrently, returning a JoinHandle to wait for completion. Use it for background tasks or parallel computations. The footgun: dropping the handle detaches the thread, risking resource leaks.
WHY IT EXISTS To allow programs to perform multiple operations at the same time by leveraging multiple CPU cores or running long-running tasks in the background without blocking the main application flow. This is the foundation of parallelism in Rust, enabling more responsive and performant applications.
THE MENTAL MODEL Think of thread::spawn as hiring a temporary worker for a specific task. You give them a set of instructions (the closure) and any tools they need (the captured variables). In return, you get a JoinHandle, which is like a claim ticket. You can use this ticket later to wait for the worker to finish and collect their result. If you throw away the ticket, the worker keeps going, but you can never check on them or get their result back.
HOW IT WORKS thread::spawn takes a closure and moves it to a new OS thread for execution. It immediately returns a JoinHandle<T>, where T is the return type of the closure. You can call .join() on this handle, which blocks the current thread until the spawned thread finishes. The result of .join() is a Result which contains the closure's return value on success, or an error if the spawned thread panicked. If the JoinHandle is dropped before .join() is called, the thread is "detached" and continues running in the background, but can no longer be joined, which can lead to resource leaks.
WHEN TO USE IT Use std::thread for heavy, CPU-bound computations that can be parallelized, or for I/O-bound tasks that would otherwise block your main thread. It's a good, low-level tool when you need direct control over OS threads. It's also frequently used with channels (std::sync::mpsc) to send data between threads.
WHEN NOT TO USE IT For managing a large number of concurrent, I/O-bound tasks, an async runtime like Tokio or async-std is often more efficient as it uses lightweight "tasks" instead of expensive OS threads. Also, if you need to configure the thread's name or stack size, you should use thread::Builder instead of the top-level spawn function.
ONE CANONICAL EXAMPLE A common pattern is to perform a calculation in a background thread. You must use the move keyword before the closure to transfer ownership of any variables from the parent scope into the new thread. This satisfies the compiler's 'static lifetime requirement, as the thread now owns its data and doesn't depend on the parent's lifetime. For example: let handle = std::thread::spawn(move || { /* expensive computation */ 42 });. You would then call handle.join().unwrap() to block until the computation is done and retrieve the value 42.
Read the original → doc.rust-lang.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.