22.xChapter 22 summary and quiz

Last updated June 13, 2026

This chapter did what almost no beginner course attempts: real concurrency, made safe by the ownership rules you already knew. Review, then a capstone.

Quick review

A thread is an independent line of execution; thread::spawn(closure) starts one running concurrently (22.1). It returns a JoinHandle whose .join() waits for the thread to finish; without joining, main returning kills spawned threads. Spawned closures usually need move, so the thread owns its data and nothing dangles, the chapter-8/9 rules across a thread boundary.

Channels pass messages between threads (22.2): mpsc::channel() gives a transmitter (tx.send) and receiver (rx.recv or iteration). send moves ownership, so a sent value can't be used by the sender again, which makes data races impossible by construction. Clone tx for multiple producers; the receiver-iterator ends when all transmitters drop.

Shared state uses Mutex plus Arc (22.3): Mutex<T> gives one-thread-at-a-time access via .lock() (the lock releases automatically on the guard's drop), and Arc<T> gives thread-safe shared ownership. Arc<Mutex<T>> is the across-threads version of chapter 21's Rc<RefCell<T>>. Rust prevents data races but not deadlock.

Send (safe to move to another thread) and Sync (safe to share by reference) are marker traits the compiler derives automatically from a type's fields (22.4). Rc isn't Send and RefCell isn't Sync, which is why you swap them for Arc and Mutex across threads. These traits are what make "fearless" a compile-time guarantee.

rayon parallelizes iterator pipelines by changing iter() to par_iter() (22.5), safe because it builds on Send/Sync. Worth it for many items of CPU-bound work; not for small or I/O-bound jobs.

Quiz time

Question #1

Why does this fail to compile, and what's the fix?

let v = vec![1, 2, 3];
let handle = std::thread::spawn(|| println!("{v:?}"));
handle.join().unwrap();
Show solution

The closure borrows v, but the spawned thread might outlive main, so the borrow could dangle (E0373). The compiler rejects it. Fix: add move, spawn(move || println!("{v:?}")), so the closure takes ownership of v and the thread keeps it alive as long as it runs.

Question #2

In a channel, why can't the sender use a value after tx.send(value)?

Show solution

send takes ownership and moves the value into the channel (chapter 8), so the sender no longer owns it; using it afterward is a use-after-move error (E0382). This guarantees the value isn't accessible to both sender and receiver simultaneously, eliminating that data race. Channels are safe because ownership transfers rather than being shared.

Question #3

What's the relationship between Arc<Mutex<T>> and Rc<RefCell<T>>, and why can't you just use Rc<RefCell<T>> across threads?

Show solution

They're the same pattern, shared ownership of mutable data, with Arc<Mutex<T>> being the thread-safe version: Arc (atomic count) replaces Rc (non-atomic), and Mutex (locking) replaces RefCell (runtime borrow flag). You can't use Rc<RefCell<T>> across threads because Rc isn't Send and RefCell isn't Sync (their bookkeeping isn't safe under concurrent access), so the compiler refuses to send them between threads (lesson 22.4).

Question #4

Rust prevents data races at compile time. Name a concurrency bug it does not prevent.

Show solution

Deadlock, two threads each holding a lock the other needs, waiting forever (also livelock, and general logic errors in ordering). These aren't memory-safety violations, so the compiler allows them; avoiding them takes discipline (consistent lock ordering, short critical sections, preferring message passing). "Fearless concurrency" means no data races, not no concurrency bugs of any kind. (Reference-cycle leaks from chapter 21 are another safe-but-unwanted case.)

Chapter capstone

Sum the squares of the numbers 1 through 1000 across multiple threads by hand, using Arc<Mutex<T>>, splitting the range among four threads. (In real code you'd use rayon; doing it manually cements the pattern.)

Show solution
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let total = Arc::new(Mutex::new(0u64));
    let mut handles = vec![];

    // Four threads, each handling a quarter of the range.
    for chunk in 0..4 {
        let total = Arc::clone(&total);
        let handle = thread::spawn(move || {
            let start = chunk * 250 + 1;
            let end = start + 250;
            let partial: u64 = (start..end).map(|n| n * n).sum();

            let mut t = total.lock().unwrap();
            *t += partial;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("sum of squares 1..=1000: {}", *total.lock().unwrap());
}
sum of squares 1..=1000: 333833500

Each thread computes the sum of squares for its own quarter of the range locally (no lock needed for that, it's the thread's own data), then locks the shared total just once to add its partial sum. Arc::clone gives each thread a handle to the same Mutex; the lock serializes the four += updates so they don't race; join waits for all four. Note the design choice that makes this efficient: do the heavy work lock-free on local data, and hold the lock only for the tiny final combine. That's the same idea rayon applies automatically, and the same answer (333833500) the sequential (1..=1000).map(|n| n*n).sum() gives.

You can now run code on many threads, pass messages, share state safely, and parallelize with one word. That's concurrency for CPU-bound work, many threads doing computation. Chapter 23 turns to the other kind: async, the model for a different problem, thousands of tasks that spend most of their time waiting (for the network, for disk, for a timer), handled efficiently without a thread apiece. It's also where, as the course has promised, Rust stops being seamless and we tell you so honestly.