Threads in Rust: thread::spawn, join and move closures
How to run code in parallel with std::thread in Rust: spawn and join, returning values, move closures, scoped threads, and the E0373 error.
A thread runs code at the same time as the rest of your program. std::thread::spawn starts one and returns a JoinHandle; calling .join() waits for the thread to finish and gives back the value its closure returned.
use std::thread;
fn main() {
let handle = thread::spawn(|| {
let mut sum = 0u64;
for i in 1..=1_000 {
sum += i;
}
sum
});
let total = handle.join().unwrap();
println!("sum is {total}");
}
sum is 500500
join() returns a Result: Err if the thread panicked. A panic in a spawned thread doesn't crash the main thread; it's reported when you join.
Giving data to a thread: move
A spawned thread may run longer than the function that started it, so its closure can't borrow local variables:
use std::thread;
fn main() {
let names = vec!["Ferris", "Corro"];
let handle = thread::spawn(|| {
println!("{} names", names.len());
});
handle.join().unwrap();
}
error[E0373]: closure may outlive the current function, but it borrows `names`, which is owned by the current function
--> src/main.rs:5:32
|
5 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `names`
6 | println!("{} names", names.len());
| ----- `names` is borrowed here
|
help: to force the closure to take ownership of `names` (and any other referenced variables), use the `move` keyword
Add move and the closure owns names. The thread can then use it as long as it likes, and main can't use it afterwards. If both need the data, clone it before spawning, or share it with Arc.
Scoped threads: borrowing safely
thread::scope guarantees that every thread spawned inside it finishes before the scope returns, so those threads may borrow local data, no move or Arc needed:
thread::scope(|s| {
for chunk in data.chunks(1000) {
s.spawn(move || process(chunk));
}
}); // all threads have finished here
This is the simplest way to split work over a slice.
Many threads, results in order
Spawn the threads, keep their handles in a Vec, then join them in order. Collecting results this way gives deterministic output even though the threads finish in any order. Printing from inside the threads doesn't: the lines interleave differently on every run.
Shared state and communication
- To send results back as they're ready: channels (
std::sync::mpsc). - To share and mutate data:
Arc<Mutex<T>>, or atomics for counters. - The compiler enforces safety through the
SendandSynctraits:RcandRefCellcan't cross threads, and the error says so.
Common mistakes
- Forgetting to join. When
mainreturns, the process exits and unfinished threads are killed mid-work. - Expecting a fixed order of output from several threads. Collect results, then print.
- Spawning a thread per tiny task. Threads cost memory (2 MB of stack each by default) and start-up time. For data parallelism,
rayon'spar_iter()splits work over a thread pool for you. - Using threads for waiting on I/O at scale. Thousands of network connections are better served by async (Tokio).
thread::sleepto "wait" for another thread. Usejoin, a channel, or a condition variable instead; sleeping is a guess.
Example
use std::thread;
fn count_primes(from: u32, to: u32) -> usize {
(from..to).filter(|&n| n >= 2 && (2..).take_while(|d| d * d <= n).all(|d| n % d != 0)).count()
}
fn main() {
// Each thread owns its range (move) and returns a value.
let ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)];
let mut handles = Vec::new();
for (from, to) in ranges {
handles.push(thread::spawn(move || (from, to, count_primes(from, to))));
}
let mut total = 0;
for h in handles {
let (from, to, n) = h.join().expect("worker panicked");
println!("{from:>6}..{to:<6} {n} primes");
total += n;
}
println!("total {total}");
// Scoped threads can borrow local data.
let words = vec!["ferris", "crab", "rustacean", "borrow"];
let lengths: Vec<usize> = thread::scope(|s| {
let handles: Vec<_> = words.iter().map(|w| s.spawn(move || w.len())).collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
println!("{words:?} -> {lengths:?}");
// A panic in a thread is reported by join, not by crashing main.
let result = thread::spawn(|| {
let empty: Vec<i32> = Vec::new();
empty[0] // panics: index out of bounds
})
.join();
println!("worker panicked: {}", result.is_err());
}
Output:
0..25000 2762 primes
25000..50000 2371 primes
50000..75000 2260 primes
75000..100000 2199 primes
total 9592
["ferris", "crab", "rustacean", "borrow"] -> [6, 4, 9, 6]
worker panicked: true