Skip to content

Concurrency

Rust’s std::thread module provides a 1:1 mapping to OS threads. Each thread gets its own stack (default 8 MB on Linux, configurable) and is scheduled by the operating system.

use std::thread;
use std::time::Duration;
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("spawned thread: {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..=3 {
println!("main thread: {}", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();

The closure passed to thread::spawn must own all captured values or borrow them for 'static. The move keyword transfers ownership into the thread’s closure:

use std::thread;
let s = String::from("hello");
let handle = thread::spawn(move || {
println!("{}", s); // s is moved into this closure
});
handle.join().unwrap();
// s is no longer valid here — it was moved

Without moveThe closure would attempt to borrow sBut the borrow checker cannot guarantee That the spawned thread will not outlive s (the thread might run after s is dropped).

JoinHandle<T> allows the spawning thread to receive the return value:

use std::thread;
let handle = thread::spawn(|| {
let mut sum = 0;
for i in 1..=100 {
sum += i;
}
sum
});
let result = handle.join().unwrap();
assert_eq!(result, 5050);

join() blocks the calling thread until the spawned thread completes. If the spawned thread panics, join() returns Err containing the panic payload.

std::thread::scope (stable since Rust 1.63) allows spawning threads that can borrow data from the Parent scope without move or 'static:

use std::sync::Mutex;
use std::thread;
let data = vec![1, 2, 3, 4, 5];
let mut results = Mutex::new(Vec::new());
thread::scope(|s| {
for chunk in data.chunks(2) {
let chunk = chunk.to_vec();
s.spawn(|| {
let sum: i32 = chunk.iter().sum();
results.lock().unwrap().push(sum);
});
}
}); // all spawned threads are joined here
assert_eq!(results, vec![3, 7, 5]);

The key guarantee: all threads spawned within scope are joined before scope returns. This means Borrowed data is guaranteed to be valid for the lifetime of the scoped threads, eliminating the need For 'static bounds.

## Intuition

Rust’s concurrency safety comes from its ownership system extended to threads. The Send marker trait indicates a type can be transferred between threads, while Sync indicates it can be shared. Channels provide message-passing concurrency, and Arc<Mutex> enables shared mutable state. The compiler prevents data races at compile time without runtime overhead, catching threading bugs that other languages only find during testing.

  • [[rust/02-ownership-borrowing/ownership]] - Ownership across thread boundaries
  • [[rust/02-ownership-borrowing/interior-mutability]] - Mutex and RwLock for shared state
  • [[rust/05-traits-generics/traits-and-generics]] - Send and Sync trait bounds
  • [[rust/06-concurrency/channels-and-message-passing]] - Channel-based communication patterns