Skip to content

Threads

In Node.js or the browser, you cannot run arbitrary JavaScript in parallel on another CPU core. You can spawn a Web Worker, but it runs in a completely separate process: you cannot share a closure or pass a reference — only serializable data can cross the boundary via postMessage.

Rust gives you real OS threads via std::thread::spawn. These threads run in the same process, share the same memory address space, and can receive values (not serialized copies) from the parent thread — as long as the ownership rules are satisfied.

thread::spawn takes a closure and runs it on a new OS thread. The closure must move any data it needs from the surrounding scope, because the thread might outlive the current stack frame.

The call returns a JoinHandle<T> where T is the return type of the closure. Calling .join() on the handle blocks until the thread finishes and returns Result<T, Box<dyn Any>>.

TypeScript
// TypeScript (Node.js): no real threads
// Web Workers run in a separate process
// and communicate only via postMessage (serialized data)
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
if (isMainThread) {
const worker = new Worker(__filename, { workerData: { value: 10 } });
worker.on('message', (result) => console.log('result:', result));
} else {
// In worker — cannot share closures or references from main thread
parentPort?.postMessage(workerData.value * 2);
}
Rust
use std::thread;
fn main() {
let value = 10u32; // owned by main thread
// move closure takes ownership of 'value'
let handle = thread::spawn(move || {
// 'value' is now owned by this thread
value * 2
});
// join() blocks until the thread completes
// and returns the closure's return value
let result = handle.join().unwrap();
println!("result: {result}"); // result: 20
}

Spawning multiple threads and collecting results

Section titled “Spawning multiple threads and collecting results”

A common pattern is to spawn a fixed number of worker threads, collect all JoinHandles into a Vec, then join them all and aggregate the results.

TypeScript
// TypeScript: parallel work via Promise.all
async function sumChunks(data: number[][]): Promise<number> {
const partials = await Promise.all(
data.map(chunk =>
new Promise<number>(resolve =>
resolve(chunk.reduce((a, b) => a + b, 0))
)
)
);
return partials.reduce((a, b) => a + b, 0);
}
Rust
use std::thread;
fn main() {
let chunks: Vec<Vec<u32>> = vec![
vec![1, 2, 3],
vec![4, 5, 6],
vec![7, 8, 9],
];
let handles: Vec<_> = chunks
.into_iter()
.map(|chunk| {
thread::spawn(move || chunk.iter().sum::<u32>())
})
.collect();
let total: u32 = handles
.into_iter()
.map(|h| h.join().unwrap())
.sum();
println!("total: {total}"); // total: 45
}
use std::thread;
fn main() {
let chunks: Vec<Vec<u32>> = vec![
vec![1, 2, 3],
vec![4, 5, 6],
vec![7, 8, 9],
];
let handles: Vec<_> = chunks
.into_iter()
.map(|chunk| {
thread::spawn(move || chunk.iter().sum::<u32>())
})
.collect();
let total: u32 = handles
.into_iter()
.map(|h| h.join().unwrap())
.sum();
println!("total: {total}");
}
What does `thread::spawn` return in Rust?
Why must closures passed to thread::spawn use the `move` keyword?
What does `.join()` on a JoinHandle do?
How does JavaScript achieve true CPU-parallel work?