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();}Output:
hello from a threadWithout 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.
Locking shared data is one way for threads to cooperate. The other common way is to not share at all, and have threads send messages to each other instead. That’s what the Channels page is about.
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.
Practice
Section titled “Practice”1. Two threads, one sum
Section titled “1. Two threads, one sum”Add up the numbers 1 to 100 using two threads: one sums 1 to 50, the other sums 51 to 100. Then main adds the two answers together. Hint: a thread’s closure can return a value, and handle.join().unwrap() gives it back to you.
Show solution
use std::thread;
fn main() { let first = thread::spawn(|| { let total: u32 = (1..=50).sum(); total }); let second = thread::spawn(|| { let total: u32 = (51..=100).sum(); total });
let a = first.join().unwrap(); let b = second.join().unwrap(); println!("{a} + {b} = {}", a + b);}Output:
1275 + 3775 = 5050These closures don’t use any outside variables, so they don’t need move. Both threads run at the same time, and join() waits for each one and hands back its result.
2. Everyone signs the guest book
Section titled “2. Everyone signs the guest book”Start three threads. Each one adds a line like "thread 2 was here" to one shared Vec<String>. After all threads finish, print the lines in sorted order. Hint: use Arc<Mutex<Vec<String>>> like the counter example, and sort a copy at the end, because the threads can finish in any order.
Show solution
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let book = Arc::new(Mutex::new(Vec::new())); let mut handles = vec![];
for id in 1..=3 { let book = Arc::clone(&book); handles.push(thread::spawn(move || { book.lock().unwrap().push(format!("thread {id} was here")); })); }
for handle in handles { handle.join().unwrap(); }
let mut lines = book.lock().unwrap().clone(); lines.sort(); for line in lines { println!("{line}"); }}Output:
thread 1 was herethread 2 was herethread 3 was hereRemove the lines.sort() line and run it a few times: the order may change. Sorting is how you get the same output every time.
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.
Quick check
Section titled “Quick check”Quick check
1. Why does a closure passed to thread::spawn usually need the move keyword?
2. What is Arc<Mutex<T>> used for?
3. What could go wrong if you forget to call .join() on a thread handle?
4. Why can the order of "main:" and "thread:" lines vary between runs of the same program?
Score: 0 / 4
Read more
Section titled “Read more”- Using Threads to Run Code Simultaneously — The Rust Book:
spawn,joinandmoveclosures, with examples of how output interleaves. - Shared-State Concurrency — The Rust Book:
MutexandArcin depth, including whyRccan’t be sent to another thread. std::thread— standard library docs: everything about threads, including naming them and scoped threads.Mutex— standard library docs: how locking works and what “poisoning” means when a thread panics while holding the lock.- Threads — Rust by Example: a short, runnable example that spawns and joins several threads.