Rust Concurrency
Concurrency — running multiple pieces of work at once — is where a lot of languages get genuinely dangerous, because two threads touching the same data at the same time can corrupt it in ways that are painfully hard to track down. This is exactly the kind of bug the borrowing rules were designed to prevent, and that protection extends directly to threads.
Spawning a thread
Section titled “Spawning a thread”use std::thread;
fn main() { let handle = thread::spawn(|| { for i in 1..=3 { println!("thread: {i}"); } });
for i in 1..=3 { println!("main: {i}"); }
handle.join().unwrap(); // wait for the thread to finish}thread::spawn takes a closure and runs it on a new thread, in parallel with everything else. .join() blocks until that thread finishes — without it, main could exit before the spawned thread ever gets to run.
Moving data into a thread
Section titled “Moving data into a thread”A closure passed to thread::spawn needs to fully own whatever data it uses, since it might run after the original scope has ended. Add the move keyword to force that:
use std::thread;
fn main() { let message = String::from("hello from a thread");
let handle = thread::spawn(move || { println!("{message}"); });
handle.join().unwrap();}Without move, Rust won’t compile this — it can’t guarantee message will still be valid by the time the new thread actually runs it. move transfers ownership of message into the closure, the same “move” you saw on the Ownership page, just handed to a thread instead of another variable.
Sharing data between threads safely
Section titled “Sharing data between threads safely”Since ordinary references don’t work across threads (they can’t outlive the scope that created them), sharing data between threads combines two things you’ve already met: Arc (from the Smart Pointers page) for shared ownership, and a Mutex to guarantee only one thread touches the data at a time:
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![];
for _ in 0..5 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); }
for handle in handles { handle.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap());}Output:
Result: 5counter.lock() waits until it can safely get exclusive access to the value, does the update, then releases it automatically. Five threads increment the same counter, and thanks to the Mutex, they never step on each other — you reliably get 5, not some smaller number from a lost update.
Why Rust calls this “fearless concurrency”
Section titled “Why Rust calls this “fearless concurrency””In most languages, nothing stops you from forgetting to lock a Mutex, or accidentally sharing a reference across threads that shouldn’t be shared. Rust’s compiler actually enforces this — code that would cause a data race across threads simply won’t compile. That’s the same ownership and borrowing system you learned earlier in this course, just applied to threads instead of a single scope.
thread::spawn(closure)runs a closure on a new thread;.join()waits for it to finish.- A closure passed to a thread usually needs
moveto take full ownership of the data it uses. - Share data across threads with
Arc(shared ownership) combined withMutex(exclusive access) —counter.lock()gets safe, one-at-a-time access to the value. - The compiler rejects code that would cause a data race at compile time, not at runtime.