21. Threads, Channels, Arc/Mutex
Rust calls its model "fearless concurrency" because the borrow checker prevents data races at compile time even across threads. This lesson covers std::thread, mpsc channels for thread-to-thread messages, and the Arc<Mutex<T>> pattern when you actually need shared state.
What you'll learn
- 1Spawn threads with `std::thread::spawn` and join them
- 2Move values into a thread via a `move` closure
- 3Pass data between threads with mpsc channels
- 4Mutate shared state safely with `Arc<Mutex<T>>`
- 5Understand the role of the Send / Sync traits
Overview
Rust's concurrency promise is **"safe even when you make mistakes."** The same borrowing rule that prevented data races in single-threaded code scales out to threads, so the compiler catches missing locks before runtime. Debugging time goes way down.
Core Concepts
1) Spawning a thread
use std::thread;
use std::time::Duration;
fn main() {
let h = thread::spawn(|| {
for i in 1..=3 {
println!("child {}", i);
thread::sleep(Duration::from_millis(50));
}
});
for i in 1..=3 {
println!("main {}", i);
thread::sleep(Duration::from_millis(50));
}
h.join().unwrap();
}2) move closures to transfer values
If a closure borrows from outside, the compiler can't prove the thread won't outlive the data. `move` transfers ownership in.
let v = vec![1, 2, 3];
let h = thread::spawn(move || println!("{:?}", v));
// v has moved into the thread
h.join().unwrap();3) mpsc channels β message passing
Standard multi-producer, single-consumer channel. Multiple senders, one receiver.
4) Arc<Mutex<T>> for shared mutable state
- **Arc<T>** β atomic reference count, shareable across threads
- **Mutex<T>** β only one thread can write at a time (runtime lock)
- **Arc<Mutex<T>>** combo β multiple threads share + mutate safely
5) Send / Sync traits
| Trait | Meaning |
|---|---|
| Send | Safe to transfer ownership across threads |
| Sync | Safe to share references across threads |
Auto-derived for nearly all standard types. Rc and RefCell are **not** Send/Sync β passing one across threads is a compile error, and Arc/Mutex are the substitute.
Hands-on Examples
Send 1~10 from a worker, sum in main:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<i32>();
thread::spawn(move || {
for i in 1..=10 { tx.send(i).unwrap(); }
});
let sum: i32 = rx.iter().sum();
println!("sum = {}", sum); // 55
}Increment a counter safely from many threads:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut hs = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
hs.push(thread::spawn(move || {
let mut n = c.lock().unwrap();
*n += 1;
}));
}
for h in hs { h.join().unwrap(); }
println!("counter = {}", *counter.lock().unwrap()); // 10
}Common Mistakes
Q. Can I pass Rc across threads?
A. No β Rc's reference count is non-atomic and races. Use Arc instead (atomic). The compiler enforces this via Send.
Q. When does a Mutex unlock?
A. When the **MutexGuard you got from `lock()` is dropped** β usually at end of scope. To release earlier use `drop(guard)` or narrow the scope with an explicit block.
Q. async/await as a replacement?
A. CPU work fits threads; IO-bound work fits async. The next lesson covers async I/O.
Recap
- Spawn with `thread::spawn` + move closure
- Pass data via mpsc channels
- Shared mutable state = Arc<Mutex<T>>
- Send/Sync traits encode thread safety at compile time
Try It Yourself
- Four threads sum 1..25, 26..50, 51..75, 76..100; aggregate via mpsc in main
- Make `Arc<Mutex<Vec<i32>>>` and have several threads push into it safely
- Try passing Rc into `thread::spawn` to read the exact compiler error
All lecture materials and example code are openly available on GitHub.
View on GitHub β