Lesson 20
Concurrency
Spawn threads, share data safely with Arc and Mutex, and experience fearless concurrency.
Fearless Concurrency
Concurrency is notoriously hard to get right -- data races, deadlocks, and subtle bugs that only appear in production. Rust's ownership system catches most of these at compile time. If your concurrent code compiles, it's free of data races. This is what Rustaceans call "fearless concurrency."
Spawning Threads
Use std::thread::spawn to create a new thread. It takes a closure (an anonymous function) that runs in the new thread. Use .join() on the returned handle to wait for the thread to finish. If you need to move data into the thread, use the move keyword before the closure -- this transfers ownership so the thread can safely use the data.
Sharing State: Arc and Mutex
Arc<T> (Atomic Reference Counted) is the thread-safe version of Rc -- it lets multiple threads share ownership of the same data. Mutex<T> provides mutual exclusion, ensuring only one thread can access the data at a time. Together, Arc<Mutex<T>> is the standard pattern for shared mutable state across threads.
Behind the scenes, Rust uses the Send and Sync traits to enforce thread safety at compile time. Send means a type can be transferred to another thread; Sync means it can be shared between threads via references. Most types implement both automatically.
Try it
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// --- Basic Thread Spawning ---
println!("--- Spawning Threads ---");
let handle = thread::spawn(|| {
let mut sum = 0;
for i in 1..=5 {
sum += i;
}
sum
});
// .join() waits for the thread and returns its result
let result = handle.join().unwrap();
println!(" Thread computed: 1+2+3+4+5 = {}", result);
// --- Move closures ---
println!("\n--- Move Closures ---");
let names = vec!["Alpha", "Beta", "Gamma"];
let handle = thread::spawn(move || {
// 'move' transfers ownership of 'names' into this thread
for name in &names {
println!(" Thread says hello to {}", name);
}
names.len()
});
let count = handle.join().unwrap();
println!(" Thread processed {} names", count);
// --- Shared State with Arc<Mutex<T>> ---
println!("\n--- Arc<Mutex<T>>: Shared State ---");
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for i in 0..5 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// .lock() acquires the mutex; the lock is released
// when the MutexGuard goes out of scope
let mut num = counter.lock().unwrap();
*num += 1;
println!(" Thread {} incremented counter to {}", i, *num);
});
handles.push(handle);
}
// Wait for all threads to finish
for handle in handles {
handle.join().unwrap();
}
println!(" Final counter value: {}", *counter.lock().unwrap());
// --- Collecting Results from Threads ---
println!("\n--- Collecting Results ---");
let results = Arc::new(Mutex::new(Vec::new()));
let mut handles = vec![];
for i in 1..=5 {
let results = Arc::clone(&results);
let handle = thread::spawn(move || {
let square = i * i;
let mut vec = results.lock().unwrap();
vec.push((i, square));
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let mut final_results = results.lock().unwrap();
final_results.sort_by_key(|&(i, _)| i);
println!(" Squares computed by threads:");
for (n, square) in final_results.iter() {
println!(" {}^2 = {}", n, square);
}
}Challenge
Modify the code to use 10 threads instead of 5 for both the counter and the squares examples. Notice how the output order might change -- threads run concurrently!
Previous: Smart Pointers