Skip to content

Threads

Rust’s standard library provides OS threads through std::thread::spawn. You pass a closure, and the function returns a JoinHandle<T> — a handle you can use to wait for the thread to finish and retrieve its return value.

use std::thread;
fn main() {
let handle = thread::spawn(|| {
// This runs on a new OS thread
42
});
let result = handle.join().unwrap(); // block until the thread finishes
println!("Thread returned: {}", result);
}

The closure is move || { ... } when it needs to capture variables from the enclosing scope — otherwise the compiler cannot guarantee the captured data lives long enough.

Because a thread may outlive the scope where it was spawned, any data the thread captures must be owned by the thread. The move keyword transfers ownership of captured variables into the closure.

use std::thread;
fn main() {
let message = String::from("hello from the main thread");
let handle = thread::spawn(move || {
// message is moved here — the thread now owns it
println!("{}", message);
});
handle.join().unwrap();
// println!("{}", message); // would not compile — moved above
}

To share mutable data between multiple threads, wrap it in Arc<Mutex<T>>:

  • Mutex<T> provides mutual exclusion — only one thread can lock it at a time.
  • Arc<T> (Atomically Reference Counted) allows multiple threads to hold a reference-counted pointer to the same allocation.

You clone the Arc before moving it into each thread. The underlying Mutex is shared, not cloned.

use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let counter = Arc::new(Mutex::new(0_i32));
let mut handles = Vec::new();
for _ in 0..4 {
let counter = Arc::clone(&counter); // cheap ref-count increment
let h = thread::spawn(move || {
let mut n = counter.lock().unwrap(); // acquire lock
*n += 1;
}); // lock released when n drops
handles.push(h);
}
for h in handles {
h.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap());
}

A common pattern is to have each thread compute a value and push it into a shared Vec. After all threads complete, you sort the results so the output is deterministic regardless of scheduling order.

use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let results = Arc::new(Mutex::new(Vec::new()));
let mut handles = Vec::new();
for i in 0..5 {
let results = Arc::clone(&results);
let handle = thread::spawn(move || {
let square = i * i;
let mut v = results.lock().unwrap();
v.push(square);
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
let mut final_results = results.lock().unwrap().clone();
final_results.sort();
println!("Squares: {:?}", final_results);
let sum: i32 = final_results.iter().sum();
println!("Sum: {}", sum);
}
What does JoinHandle::join() do?
Why do thread closures typically use the move keyword?
What is the role of Arc in Arc<Mutex<T>>?
Why do we sort results after joining all threads?