Rust — Threads
Real parallelism, safely
thread::spawn starts an OS thread — code genuinely runs simultaneously on other cores:
use std::thread;
fn main() {
let handle = thread::spawn(|| {
for i in 1..5 {
println!("worker: {}", i);
}
});
println!("main still running");
handle.join().unwrap(); // wait for the worker to finish
}
Without join(), main may exit first and kill the worker mid-flight. join blocks until the thread completes.
Moving data into threads
Closures capture environment; threads outlive ambiguity, so Rust demands move:
let numbers = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("got {:?}", numbers); // ownership transferred INTO the thread
});
// println!("{:?}", numbers); ❌ moved!
handle.join().unwrap();
move forces ownership transfer — the borrow checker guarantees the thread never references freed stack data. This single keyword eliminates a whole C++ footgun class.
Returning values from threads
JoinHandle returns the closure's result:
let result = thread::spawn(move || {
(1..=100).sum::<i64>()
}).join().unwrap();
println!("sum: {}", result); // 5050
.join() returns Result<T> (thread may panic) — unwrap() or propagate.
Sharing MUTABLE data — Arc<Mutex<T>>
Two threads can't both own one value. The standard pattern wraps it:
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter); // cheap ref-counted clone
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
})); // MutexGuard unlocks on drop
}
for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap()); // 10 — always exactly 10
| Piece | Job |
|---|---|
Arc | Atomic Reference Counting — share across threads |
Mutex | exclusive access — only one thread mutates at a time |
.lock().unwrap() | acquire; blocks until free |
| guard drop | automatic unlock at end of scope |
Rust makes you prove safety through types instead of hoping discipline holds — data races are compile errors here.
Channels — passing messages instead
Often better than shared state: threads communicate by sending values:
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(42).unwrap();
});
println!("{}", rx.recv().unwrap()); // 42
Full treatment in the channels lesson.
Threads vs async (preview)
| OS threads | async/await | |
|---|---|---|
| Cost | ~MB stack each | tiny task structs |
| Best for | CPU-bound work | thousands of I/O waits |
| Runtime | built into std | needs tokio etc. |
Mini Practice
- Spawn 4 threads each printing their id 3 times; join all.
- Move a vector in; sum inside; return via join.
- Race condition demo without Mutex (compile error proves it) — then fix with Arc<Mutex>.
- Channel: producer sends 1..=5, main receives all.
Next: channels →
Related Topics
Frequently Asked Questions about Threads
What is Threads in Rust?
Threads is a fundamental concept in Rust. This lesson explains it step by step with clear examples, making it easy for beginners to understand.
How do I learn Threads?
Start by reading the explanation above, then try the code examples. Practice by modifying the examples and experimenting with different values. Hands-on practice is the best way to learn Threads.
Why is Threads important in Rust?
Threads is essential for Rust development. Understanding this concept will help you write better code and solve real-world problems more effectively.