Concurrency

Course 2 · Ch 5
Concurrency
Where Chapter 4's forward pointer to Arc<T> pays off — "fearless concurrency," made concrete

Chapter 4 closed with a forward pointer to Arc<T> for thread-safety. This chapter is where that payoff lands: the same ownership and borrowing rules that prevented aliasing bugs in single-threaded code (Course 1, Chapter 4) also prevent data races across threads — enforced at compile time, not discovered at runtime.

Spawning Threads

use std::thread; let handle = thread::spawn(|| { println!("Hello from a spawned thread!"); }); println!("Hello from main!"); handle.join().unwrap(); // waits for the spawned thread to finish

thread::spawn returns a JoinHandle; .join() blocks until that thread completes.

The move Keyword and Ownership Across Threads

A spawned thread might genuinely outlive the function that spawned it — the compiler can't assume it won't. A closure that only borrows data risks that data being dropped before the thread finishes using it, so thread::spawn typically requires a move closure, forcing ownership of captured variables into the thread itself:

let data = vec![1, 2, 3]; // Without `move`, this often fails to compile — the closure only // borrows `data`, and the compiler can't guarantee it outlives the thread. let handle = thread::spawn(move || { println!("{:?}", data); // data is now OWNED by this closure }); handle.join().unwrap();

This is Course 1's move semantics (Chapter 3) and borrow-checker lifetime reasoning (Chapter 4), both directly enforced here — applied to a genuinely new context where a thread's actual lifetime is unpredictable.

Sharing Data Safely with Arc<Mutex<T>>

Chapter 4's Rc<RefCell<T>> pattern has a thread-safe sibling: Arc<T> ("Atomic Reference Counted") replaces Rc, and Mutex<T> replaces RefCell — using a real OS-level lock, where .lock() returns a guard that unlocks automatically when dropped:

use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; })); } for h in handles { h.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); // 10

mpsc Channels: Message Passing

std::sync::mpsc::channel() ("multiple producer, single consumer") returns a (Sender, Receiver) pair — threads communicate by sending owned values rather than sharing memory directly:

use std::sync::mpsc; use std::thread; let (tx, rx) = mpsc::channel(); thread::spawn(move || { tx.send(String::from("hello from the thread")).unwrap(); }); let received = rx.recv().unwrap(); println!("{}", received);

This is a direct expression of a proverb closely associated with Go's own community: "Do not communicate by sharing memory; instead, share memory by communicating." Rust and Go arrive at genuinely similar channel-based thinking here.

Contrast With Go's Goroutines & Channels (go2-3)

Go's goroutines are lightweight, green-thread-style tasks managed by Go's own runtime scheduler — many goroutines multiplex onto relatively few real OS threads. Rust's thread::spawn creates real OS threads directly — heavier per-thread, with no separate runtime scheduler required (Rust's own lightweight async tasks are Course 3's territory, a different mechanism entirely). Go's channels are first-class language syntax (ch <- value, <-ch); Rust's mpsc channels are an ordinary standard-library type, no special syntax needed.

The more important contrast: Go's concurrency safety net is best-effort — its race detector catches some data races, opt-in, typically during testing. Rust's guarantee is enforced by the compiler itself, for every concurrent program, as a hard requirement just to compile at all.

GoRust
Thread modelLightweight goroutines, runtime-scheduledReal OS threads (async tasks are separate, Course 3)
ChannelsFirst-class language syntaxA standard-library type (mpsc)
Data-race safetyBest-effort — race detector, opt-in/testing-timeCompiler-enforced — required to compile at all

thread::spawn / join

Creates a real OS thread; join() blocks until it finishes.

move Closures

Forces ownership transfer into the thread — required since a thread's lifetime is unpredictable.

Arc<Mutex<T>>

The thread-safe sibling of Rc<RefCell<T>> — shared, lockable mutable state.

mpsc Channels

Send owned values between threads instead of sharing memory directly.

The Actual Mechanism Behind "Fearless Concurrency"
Two marker traits, Send (safe to move to another thread) and Sync (safe to share a reference across threads), are what the compiler actually checks. Trying to share a plain Rc<T> — not thread-safe, per Chapter 4's warning — across threads is a compile error, not a data race waiting to happen at runtime. This is the concrete mechanism behind Rust's "fearless concurrency" claim: it isn't a slogan, it's Send/Sync doing real, checked work.
Mutex<T> Prevents Data Races, Not Deadlocks
Two threads, each holding one lock and waiting for the other's lock, will deadlock — and Rust's compiler does not catch this. The compile-time guarantee is specifically about memory safety and data races, not general concurrency correctness. A deadlock is a real, possible runtime bug that the type system has no visibility into at all — an honest limit on what "fearless concurrency" actually promises.

Coding Challenges

Challenge 1

Write code that spawns a thread using a move closure to print a Vec created in main, then explain what compiler error would occur (in general terms) if move were removed.

📄 View solution
Challenge 2

Using Arc>>, spawn 5 threads that each push their own thread number (0-4) onto a shared vector, join all threads, then print the vector's final length and confirm it's 5.

📄 View solution
Challenge 3

Explain why Rust's compile-time thread safety is a stronger guarantee than Go's race detector, but is still not a complete guarantee of concurrency correctness — referencing deadlocks specifically as an example of what it doesn't cover.

📄 View solution

Chapter 5 Quick Reference

  • thread::spawn(|| {...}) — creates a real OS thread; returns a JoinHandle; .join() waits for completion
  • move closures — required to transfer ownership into a thread, since its lifetime is unpredictable
  • Arc<T> — thread-safe Rc; Mutex<T> — thread-safe RefCell, using a real OS lock
  • Arc<Mutex<T>> — the multi-threaded analog of Rc<RefCell<T>>
  • mpsc::channel() — a (Sender, Receiver) pair; send owned values instead of sharing memory
  • Send/Sync — the marker traits the compiler checks to reject unsafe cross-thread sharing at compile time
  • Go's race safety is best-effort tooling; Rust's is a hard, compiler-enforced compile requirement
  • Mutex<T> prevents data races, not deadlocks — a real, uncaught category of concurrency bug remains possible
  • Next chapter: Modules & Crates — the module system, Cargo.toml, workspaces, and publishing to crates.io