Concurrency & Threads
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — that last phrase means its type system stops data races at compile time, so multithreaded code is safe by construction.
Learn Concurrency & Threads in our free Rust course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Rust course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
In this lesson you'll spawn threads, return values from them, pass messages with channels, and share state safely with Arc and Mutex .
What You'll Learn in This Lesson
1️⃣ Spawning Threads and Joining
thread::spawn runs a closure on a new thread and returns a JoinHandle . Call .join() to wait for it to finish and collect its return value. When a thread needs to use outside data, the closure must take ownership of it with move , since the thread may outlive the current scope.
The second thread used move to take ownership of data — that's how the compiler guarantees the vector outlives the thread that reads it.
2️⃣ Channels and Shared State
There are two main ways threads coordinate. Channels ( mpsc ) pass messages: a sender hands values to a receiver. For shared mutable state , wrap the data in an Arc<Mutex<T>> — Arc shares ownership across threads, and Mutex ensures only one thread accesses it at a time.
Five threads each incremented the same counter, and the result was reliably 5 — no data race — because the Mutex serialized access. Try writing that safely in another language and you'll appreciate "fearless".
Your turn. Fill in the blanks marked ___ , then run it.
Mini-Challenge: Parallel Sum
Use Arc<Mutex<i32>> and four threads to accumulate a shared total. Run it with cargo run .
Common Errors (and the fix)
- "closure may outlive the current function" — Add move so the thread owns the data it captures.
- " cannot be sent between threads safely" — Use Arc instead of Rc across threads.
- Program ends before a thread finishes — Hold onto the JoinHandle and call .join() so main waits.
- Deadlock from holding a lock too long — Keep the locked scope small; the lock releases when the guard from .lock() is dropped.
Pro Tips
- 💡 Prefer channels when you can: message passing often beats shared state for clarity and safety.
- 💡 Arc<Mutex<T>> is the shared-state pattern: Arc to share, Mutex to mutate.
- 💡 Always join your handles: joining ensures threads complete and surfaces any panics.
📋 Quick Reference — Concurrency
Goal
Rust Syntax
Spawn a thread
thread::spawn(|| { })
Wait for it
handle.join().unwrap()
Own captures
thread::spawn(move || { })
Channel
let (tx, rx) = mpsc::channel();
Share ownership
Arc::new(x)
Lock for mutation
m.lock().unwrap()
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ thread::spawn runs a closure on a new thread; join waits for it
- ✅ Thread closures use move to own captured data
- ✅ Channels ( mpsc ) pass messages between threads
- ✅ Arc<Mutex<T>> shares mutable state safely; data races are caught at compile time
- ✅ Next lesson: Capstone — build a real command-line tool
Practice quiz
What does thread::spawn return?
- The thread's result directly
- Nothing
- A JoinHandle you can wait on
- A Mutex
Answer: A JoinHandle you can wait on. spawn returns a JoinHandle; calling .join() waits for the thread and yields its result.
What does handle.join() do?
- Waits for the thread to finish and returns its result (a Result)
- Starts a new thread
- Kills the thread immediately
- Locks shared data
Answer: Waits for the thread to finish and returns its result (a Result). join blocks until the thread completes and returns a Result containing its value.
Why do thread closures usually need the move keyword?
- For speed
- To avoid imports
- Because closures cannot borrow at all
- Because the thread may outlive the current scope, so it must own the data it uses
Answer: Because the thread may outlive the current scope, so it must own the data it uses. move makes the closure own its captures so the data lives as long as the thread.
What does 'fearless concurrency' refer to in Rust?
- Threads never crash
- The type system prevents data races at compile time
- Threads are always faster
- There is no need to join
Answer: The type system prevents data races at compile time. Ownership and borrowing rules turn would-be data races into compile errors.
What is the difference between Rc and Arc?
- Rc is single-threaded; Arc is thread-safe (atomic) for sharing across threads
- No difference
- Arc is single-threaded; Rc is for threads
- Arc cannot be cloned
Answer: Rc is single-threaded; Arc is thread-safe (atomic) for sharing across threads. Arc is the atomically reference-counted, thread-safe equivalent of Rc.
Why wrap shared data in a Mutex when sharing across threads?
- To clone it faster
- To sort it
- To ensure only one thread accesses the data at a time
- To avoid using Arc
Answer: To ensure only one thread accesses the data at a time. A Mutex serializes access so only the lock holder can read or write the data.
What is the idiomatic pattern for shared MUTABLE state across threads?
- Rc<RefCell<T>>
- Arc<Mutex<T>>
- Box<T>
- Vec<T>
Answer: Arc<Mutex<T>>. Arc shares ownership across threads and Mutex coordinates safe mutation.
What does mpsc::channel() give you?
- A single value
- Two threads
- A locked Mutex
- A transmitter (tx) and a receiver (rx) for passing messages
Answer: A transmitter (tx) and a receiver (rx) for passing messages. It returns a (tx, rx) pair; senders push values to tx and a receiver reads from rx.
When does iterating over a channel receiver (for x in rx) stop?
- After one message
- When all senders are dropped
- After 60 seconds
- Never
Answer: When all senders are dropped. Receiving iterates until every sender has been dropped, then the loop ends.
When is a Mutex lock released?
- Never automatically
- Only by calling unlock()
- When the guard returned by .lock() is dropped (goes out of scope)
- When the program exits
Answer: When the guard returned by .lock() is dropped (goes out of scope). The lock is held by the guard and released automatically when that guard is dropped.
Continue this course
- Previous: Smart Pointers (Box, Rc, RefCell)
- Next: Capstone: A CLI Tool