Fearless Concurrency in Rust
January 1, 2026Part 1: Understanding the Problem First
Before threads make sense, you need to understand why we need them.
The Single-Threaded World
When you run a normal Rust program, it does one thing at a time, in order:
fn main() {
println!("Step 1");
println!("Step 2");
println!("Step 3");
}
This always prints 1, then 2, then 3. Simple and predictable.
The Problem: Waiting
What happens when your program needs to wait?
use std::thread;
use std::time::Duration;
fn main() {
println!("Starting task 1...");
thread::sleep(Duration::from_secs(3)); // Wait 3 seconds
println!("Task 1 done!");
println!("Starting task 2...");
thread::sleep(Duration::from_secs(3)); // Wait 3 seconds
println!("Task 2 done!");
}
Total time: 6 seconds. Your program just sits there waiting.
Visual:
Time: 0s 1s 2s 3s 4s 5s 6s
│───────────────────────│───────────────────────│
[ Task 1: waiting ][ Task 2: waiting ]
Total: 6 seconds
The Solution: Do Both at Once
What if we could do both tasks at the same time?
Time: 0s 1s 2s 3s
│───────────────────────│
[ Task 1: waiting ]
[ Task 2: waiting ]
Total: 3 seconds (they overlap!)
This is concurrency: doing multiple things during overlapping time periods.
Real-World Examples
Programs often need to:
- Download multiple files at once
- Respond to clicks while doing background work
- Handle many network connections simultaneously
- Process thousands of records quickly
Without concurrency, your program does one thing, waits, then does the next. Slow!
Part 2: What Is a Thread?
The Analogy: Workers
Think of your program as a kitchen:
Single-threaded = One chef does everything:
Chef: [chop vegetables][wait for water to boil][cook pasta][make sauce]
Total time: Very long (tasks happen one after another)
Multi-threaded = Multiple chefs:
Chef 1: [chop vegetables][make sauce]
Chef 2: [boil water][cook pasta]
Total time: Much shorter (tasks overlap!)
What Is a Thread, Technically?
A thread is an independent worker that can run code on its own.
Your program always starts with one thread - the main thread.
Program starts
│
▼
┌───────────────┐
│ Main Thread │ ← Every program has this
└───────────────┘
You can create additional threads called spawned threads:
Program starts
│
▼
┌───────────────┐
│ Main Thread │
└───────┬───────┘
│ spawn
├──────────────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Thread 1 │ │ Thread 2 │
└───────────────┘ └───────────────┘
All three run at the same time!
Important: Unpredictable Order
When multiple threads run, you cannot predict the order. The operating system decides.
use std::thread;
fn main() {
thread::spawn(|| println!("A"));
thread::spawn(|| println!("B"));
println!("C");
}
Possible outputs:
C A BA C BC B AA B CB A CB C A
Every ordering is possible! This unpredictability is what makes concurrency tricky.
Part 3: Creating Your First Thread
The Simplest Example
use std::thread;
fn main() {
thread::spawn(|| {
println!("Hello from new thread!");
});
println!("Hello from main!");
}
Let me explain every piece:
use std::thread;
Import the thread module from Rust's standard library.
thread::spawn(...)
A function that creates a new thread. "Spawn" means "bring into existence."
It takes one argument: the code you want the new thread to run.
|| { ... }
This is a closure - a mini-function you create on the spot.
// Regular function
fn say_hello() {
println!("Hello!");
}
// Closure (same thing, different syntax)
|| {
println!("Hello!");
}
The || means "no parameters." The { ... } is the code to run.
Visual of what happens:
Time 0: Program starts
Main thread begins
Time 1: Main calls thread::spawn(...)
NEW THREAD IS CREATED!
Now two threads exist
Time 2: Both threads run (order unpredictable)
Main thread: println!("Hello from main!")
New thread: println!("Hello from new thread!")
Time 3: Main thread ends
PROGRAM EXITS
All threads killed!
The Problem: Disappearing Output
Run the code above multiple times. Sometimes you'll see:
Hello from main!
Hello from new thread!
Sometimes just:
Hello from main!
Where did the spawned thread's message go?
Why Output Disappears
When the main thread ends, the entire program ends. All spawned threads are killed immediately, even if they haven't finished!
Scenario where output is lost:
Main thread: [spawn][print "main"][END]
│ ↑
│ Program exits here!
│
Spawned thread: └──[still starting up...][KILLED!]
↑
Never got to print!
The spawned thread was still warming up when main finished.
Part 4: Waiting for Threads - JoinHandle
The Solution
thread::spawn returns a JoinHandle. Think of it as a ticket for your thread.
You can use this ticket to wait for the thread to finish.
use std::thread;
fn main() {
// Save the ticket (JoinHandle)
let handle = thread::spawn(|| {
println!("Hello from new thread!");
});
println!("Hello from main!");
// Wait for the thread to finish
handle.join().unwrap();
println!("Thread finished, exiting!");
}
Output (always):
Hello from main!
Hello from new thread!
Thread finished, exiting!
Breaking Down join()
let handle = thread::spawn(...);
We save the JoinHandle. Type is JoinHandle<()> - the () means the thread returns nothing.
handle.join()
This makes the current thread stop and wait until the spawned thread finishes.
"Join" means "wait for this thread to come back."
.unwrap()
join() returns a Result because the spawned thread might have panicked (crashed).
Ok(value)- thread finished normallyErr(e)- thread panicked
.unwrap() gets the Ok value, or panics if there was an error.
Visual: With and Without join()
Without join():
Main thread: [spawn][print][END]
│ ↑
│ Program exits!
│
Spawned thread: └──[print]───??? (might not finish)
With join():
Main thread: [spawn][print][waiting.......][print][END]
│ ↑
│ Blocked here until
│ spawned thread finishes
│
Spawned thread: └──────[print][DONE]
↑
join() returns here
Longer Example
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
println!("Spawned: Starting work...");
thread::sleep(Duration::from_secs(2));
println!("Spawned: Done!");
});
println!("Main: Waiting for thread...");
handle.join().unwrap(); // Blocks for ~2 seconds
println!("Main: Thread finished!");
}
Output:
Main: Waiting for thread...
Spawned: Starting work...
[2 second pause]
Spawned: Done!
Main: Thread finished!
The main thread waits at join() for 2 seconds while the spawned thread works.
Part 5: Moving Data Into Threads
The Problem
What if your thread needs data from outside?
use std::thread;
fn main() {
let name = String::from("Alice");
thread::spawn(|| {
println!("Hello, {}!", name); // Using name from main
});
}
This won't compile! Error:
closure may outlive the current function, but it borrows `name`
Why This Fails
By default, closures borrow variables they use. But there's a problem:
Main thread: [create name][spawn][drop name][END]
│ ↑
│ name is gone!
│
Spawned thread: └──[try to use name]
↑
name doesn't exist anymore!
The spawned thread might try to use name after main has dropped it. That's a use-after-free bug!
Rust prevents this at compile time.
The Solution: move
The move keyword tells the closure to take ownership instead of borrowing:
use std::thread;
fn main() {
let name = String::from("Alice");
thread::spawn(move || { // move keyword added!
println!("Hello, {}!", name);
});
// Can't use name here anymore, it was moved!
// println!("{}", name); // ERROR: value moved
}
Visual: Borrowing vs Moving
Without move (borrowing - won't compile):
┌─────────────────┐ ┌─────────────────┐
│ Main Thread │ │ Spawned Thread │
│ │ │ │
│ ┌───────────┐ │ borrow │ ┌───────────┐ │
│ │ name │◄─┼─────────┼──│ reference │ │
│ │ "Alice" │ │ │ └───────────┘ │
│ └───────────┘ │ │ │
│ │ │ What if main │
│ name lives │ │ drops name? │
│ here │ │ DANGER! │
└─────────────────┘ └─────────────────┘
With move (ownership transfer - compiles):
┌─────────────────┐ ┌─────────────────┐
│ Main Thread │ │ Spawned Thread │
│ │ │ │
│ │ moved │ ┌───────────┐ │
│ (name is gone) │ ═══════►│ │ name │ │
│ │ │ │ "Alice" │ │
│ │ │ └───────────┘ │
│ │ │ │
│ Can't use name │ │ OWNS name, │
│ anymore │ │ safe! │
└─────────────────┘ └─────────────────┘
The spawned thread now owns the data. It can use it whenever it wants.
What About Copy Types?
For types that implement Copy (integers, booleans, etc.), move makes a copy:
use std::thread;
fn main() {
let count = 42; // i32 implements Copy
thread::spawn(move || {
println!("Count: {}", count); // Gets a COPY
});
println!("Original: {}", count); // Still works!
}
| Type | What move Does |
|---|---|
String, Vec, Box |
Transfers ownership (original gone) |
i32, bool, char |
Makes a copy (original still usable) |
Part 6: Threads Need to Communicate
So far, threads work independently. But what if they need to share information?
Two approaches:
| Approach | How It Works | Analogy |
|---|---|---|
| Message Passing | Send data through a channel | Passing notes |
| Shared State | Access the same memory | Shared whiteboard |
There's a famous saying: "Do not communicate by sharing memory; share memory by communicating."
Let's start with message passing.
Part 7: Channels - Message Passing
The Analogy: Mailbox
Think of a channel like a mailbox:
┌─────────────────┐ ┌─────────────────┐
│ Sender Thread │ │ Receiver Thread │
│ │ │ │
│ "I have a │ ┌─────────┐ │ "I'll check │
│ message!" │────►│ MAILBOX │◄─────│ the mailbox" │
│ │ └─────────┘ │ │
│ (puts letter │ │ (takes letter │
│ in mailbox) │ │ from mailbox) │
└─────────────────┘ └─────────────────┘
- Sender puts messages in
- Receiver takes messages out
- Messages flow one direction
Creating a Channel
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
use std::sync::mpsc;
Import the channel module. mpsc stands for Multiple Producer, Single Consumer:
- Multiple Producer: Many threads can send
- Single Consumer: Only one thread receives
mpsc::channel()
Creates a channel. Returns a tuple of two things:
tx- the transmitter (sender)rx- the receiver
let (tx, rx) = ...
Tuple destructuring to get both parts.
Visual:
┌────────────────────────────────────────┐
│ Channel │
│ │
│ tx ──────────────────────► rx │
│ (send) (receive) │
│ │
└────────────────────────────────────────┘
Sending and Receiving
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let message = String::from("Hello!");
tx.send(message).unwrap();
println!("Sent the message!");
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
Output:
Sent the message!
Got: Hello!
Breaking It Down
tx.send(message)
Puts message into the channel.
Returns Result<(), SendError>:
Ok(())- sent successfullyErr(...)- receiver was dropped (no one to receive)
Critical: After sending, message is gone! The sender can't use it anymore.
tx.send(message).unwrap();
// println!("{}", message); // ERROR! value moved
rx.recv()
Takes a message from the channel.
Important: If no message is available, recv() blocks (waits) until one arrives!
Returns Result<T, RecvError>:
Ok(message)- got a messageErr(...)- all senders dropped (channel closed)
Visual: Ownership Transfer
When you send a message, ownership transfers:
BEFORE send():
┌─────────────────┐ ┌─────────────────┐
│ Sender Thread │ │ Receiver Thread │
│ │ │ │
│ ┌───────────┐ │ │ │
│ │ message │ │ │ (waiting...) │
│ │ "Hello" │ │ │ │
│ └───────────┘ │ │ │
└─────────────────┘ └─────────────────┘
AFTER send():
┌─────────────────┐ ┌─────────────────┐
│ Sender Thread │ │ Receiver Thread │
│ │ │ │
│ │ ══════► │ ┌───────────┐ │
│ (message gone) │ │ │ message │ │
│ │ │ │ "Hello" │ │
│ │ │ └───────────┘ │
└─────────────────┘ └─────────────────┘
The sender cannot use the message after sending. This prevents data races!
recv() Blocks
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
thread::sleep(Duration::from_secs(2)); // Wait 2 seconds
tx.send(String::from("Delayed!")).unwrap();
});
println!("Waiting for message...");
let msg = rx.recv().unwrap(); // BLOCKS HERE for 2 seconds
println!("Got: {}", msg);
}
Output:
Waiting for message...
[2 second pause]
Got: Delayed!
The main thread stops at recv() and waits.
Non-Blocking: try_recv()
If you don't want to wait, use try_recv():
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
// Don't send anything yet
match rx.try_recv() {
Ok(msg) => println!("Got: {}", msg),
Err(mpsc::TryRecvError::Empty) => println!("No message yet!"),
Err(mpsc::TryRecvError::Disconnected) => println!("Channel closed!"),
}
}
Output:
No message yet!
try_recv() returns immediately, doesn't wait.
| Method | Behavior |
|---|---|
recv() |
Waits until message arrives |
try_recv() |
Returns immediately (might be empty) |
Sending Multiple Messages
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let messages = vec!["one", "two", "three"];
for msg in messages {
tx.send(msg).unwrap();
}
// tx is dropped here, closing the channel
});
// rx works as an iterator!
for received in rx {
println!("Got: {}", received);
}
println!("Channel closed!");
}
Output:
Got: one
Got: two
Got: three
Channel closed!
for received in rx
The receiver implements Iterator! The loop:
- Calls
recv()for each iteration - Ends when the channel closes (all senders dropped)
Multiple Senders
What if multiple threads want to send? Clone the transmitter!
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone(); // Create second sender
thread::spawn(move || {
tx.send("from thread 1").unwrap();
});
thread::spawn(move || {
tx2.send("from thread 2").unwrap();
});
for msg in rx {
println!("Got: {}", msg);
}
}
Output (order may vary):
Got: from thread 1
Got: from thread 2
Visual:
┌───────────────┐
│ Thread 1 │───┐
│ (tx) │ │
└───────────────┘ │
▼
┌──────────┐ ┌───────────────┐
│ Channel │─────►│ Main │
└──────────┘ │ (rx) │
▲ └───────────────┘
┌───────────────┐ │
│ Thread 2 │───┘
│ (tx2) │
└───────────────┘
Multiple senders, one receiver. Channel closes when all senders are dropped.
Part 8: Shared State - Mutex
When Channels Aren't Enough
Channels work great when data flows one direction. But sometimes multiple threads need to access the same data:
- A counter that multiple threads increment
- A cache that threads read and update
- A configuration that threads check
The Danger: Data Races
A data race happens when:
- Two threads access the same data
- At least one is writing
- No synchronization
Example of what goes wrong:
Counter starts at 0. Two threads increment it.
WHAT WE WANT:
Thread A: read(0) → add 1 → write(1)
Thread B: read(1) → add 1 → write(2)
Result: 2 ✓
WHAT CAN HAPPEN:
Thread A: read(0)
Thread B: read(0) ← Both read BEFORE either writes!
Thread A: add 1 → write(1)
Thread B: add 1 → write(1) ← Overwrites A's work!
Result: 1 ✗ (should be 2)
This is a data race - unpredictable, hard to debug.
The Solution: Mutex
A Mutex (mutual exclusion) is like a bathroom with a lock:
- Before entering, you lock the door
- While you're inside, no one else can enter
- When done, you unlock the door
- Next person waiting can now enter
┌─────────────────────────────────────────┐
│ Mutex │
│ ┌─────────────────────────────────┐ │
│ │ 🔒 LOCKED 🔒 │ │
│ │ │ │
│ │ ┌───────────────────┐ │ │
│ │ │ data: 42 │ │ │
│ │ └───────────────────┘ │ │
│ │ │ │
│ │ Only one thread can │ │
│ │ access at a time! │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
Basic Mutex Usage
use std::sync::Mutex;
fn main() {
let counter = Mutex::new(0); // Wrap data in Mutex
{
let mut num = counter.lock().unwrap(); // Lock it
*num += 1; // Change the data
println!("Value: {}", *num);
} // Lock automatically released here!
println!("Final: {}", *counter.lock().unwrap());
}
Output:
Value: 1
Final: 1
Breaking It Down
Mutex::new(0)
Creates a Mutex containing the value 0. The data is "locked inside."
┌─────────────┐
│ Mutex │
│ ┌───────┐ │
│ │ 0 │ │ ← Data is inside
│ └───────┘ │
└─────────────┘
counter.lock()
Tries to acquire the lock:
- If available: returns immediately with access
- If another thread has it: blocks (waits) until they release
Returns Result<MutexGuard, PoisonError>.
let mut num = counter.lock().unwrap();
num is a MutexGuard - a smart pointer that:
- Lets you access the inner data with
*num - Automatically unlocks when dropped
*num += 1;
Dereference to access/modify the inner value.
The { } scope
When num goes out of scope, it's dropped. The Drop trait releases the lock automatically!
{
let mut num = counter.lock().unwrap();
*num += 1;
} // num dropped here → lock released!
You can't forget to unlock. Rust handles it!
The Problem: Sharing Mutex Between Threads
Let's try to share a Mutex:
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
for _ in 0..5 {
thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
}
}
Won't compile! Error:
use of moved value: `counter`
Why? First loop iteration moves counter into the thread. Second iteration tries to move it again - but it's gone!
Part 9: Arc - Thread-Safe Sharing
The Problem Restated
We need multiple owners of the same Mutex.
Remember Rc from Chapter 15? It allows multiple owners. But Rc is not thread-safe - its reference counting operations can have data races!
The Solution: Arc
Arc = Atomic Reference Counting
Same concept as Rc, but uses atomic operations that are safe across threads.
| Type | Thread-Safe? | Use For |
|---|---|---|
Rc<T> |
No | Single-threaded sharing |
Arc<T> |
Yes | Multi-threaded sharing |
Using Arc with Mutex
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0)); // Arc around Mutex
let mut handles = vec![];
for i in 0..5 {
let counter_clone = Arc::clone(&counter); // Clone the Arc
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
println!("Thread {} incremented to {}", i, *num);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final: {}", *counter.lock().unwrap());
}
Output:
Thread 0 incremented to 1
Thread 1 incremented to 2
Thread 2 incremented to 3
Thread 3 incremented to 4
Thread 4 incremented to 5
Final: 5
Visual: Arc<Mutex>
Main Thread Thread 1 Thread 2
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Arc │ │ Arc │ │ Arc │
│ count=3 │ │ count=3 │ │ count=3 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌─────────────┐
│ Mutex │
│ ┌───────┐ │
│ │ 0 │ │ ← Actual data
│ └───────┘ │
└─────────────┘
All Arcs point to the same Mutex. Only one thread can lock it at a time.
Why Both Arc AND Mutex?
| Tool | What It Does | What It Doesn't Do |
|---|---|---|
Arc |
Shared ownership across threads | Allow mutation |
Mutex |
Safe mutation | Shared ownership |
Together: multiple threads can own (Arc) and mutate (Mutex) the same data safely!
Part 10: Deadlocks
The Danger
Mutexes can cause deadlocks - threads waiting for each other forever.
Thread A:
1. Lock resource_x ✓
2. Try to lock resource_y... (waiting for B)
Thread B:
1. Lock resource_y ✓
2. Try to lock resource_x... (waiting for A)
Both wait forever!
Rust cannot prevent deadlocks at compile time. They're logic errors, not memory errors.
How to Avoid
- Always lock in the same order - if everyone locks X before Y, no circular waiting
- Hold locks briefly - lock, do work, unlock quickly
- Avoid nested locks - fewer simultaneous locks = less risk
Part 11: Send and Sync Traits
What Are They?
Two marker traits that Rust uses to ensure thread safety:
| Trait | Meaning |
|---|---|
Send |
Safe to transfer ownership to another thread |
Sync |
Safe to reference from multiple threads |
How They Protect You
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(5);
thread::spawn(move || {
println!("{}", data);
});
}
Compile error:
`Rc<i32>` cannot be sent between threads safely
The compiler automatically prevents unsafe types from crossing thread boundaries!
Automatic Implementation
You rarely implement these manually. Rust derives them automatically:
struct Safe {
name: String, // Send + Sync
count: i32, // Send + Sync
}
// Safe is automatically Send + Sync!
struct NotSafe {
data: Rc<i32>, // NOT Send, NOT Sync
}
// NotSafe is automatically NOT Send, NOT Sync!
If all fields are Send, the struct is Send. Same for Sync.
Quick Reference
| Type | Send? | Sync? | Why |
|---|---|---|---|
i32, bool |
✅ | ✅ | Simple values |
String, Vec<T> |
✅ | ✅ | Owned heap data |
Rc<T> |
❌ | ❌ | Non-atomic reference count |
Arc<T> |
✅ | ✅ | Atomic reference count |
Mutex<T> |
✅ | ✅ | Designed for threads |
RefCell<T> |
✅ | ❌ | Runtime borrow check not thread-safe |
Part 12: When to Use What
Decision Guide
Do you need to share data between threads?
│
├── NO → Just use `move` closures
│ Each thread works independently
│
└── YES → Do threads need to MODIFY the data?
│
├── NO (read-only) → Arc<T>
│ Multiple threads read the same data
│
└── YES → Choose communication style:
│
├── Message Passing → mpsc::channel
│ • Data flows one direction
│ • Clear sender/receiver
│
└── Shared State → Arc<Mutex<T>>
• Multiple threads modify same data
• Any thread can access anytime
Summary Table
| Situation | Tool | Example |
|---|---|---|
| Independent threads | move closures |
Parallel calculations |
| Read-only shared data | Arc<T> |
Shared configuration |
| One sender, one receiver | mpsc::channel |
Worker sends results to main |
| Many senders, one receiver | channel + clone |
Multiple workers → collector |
| Multiple threads modify same data | Arc<Mutex<T>> |
Shared counter, cache |
The Complete Toolkit
| Tool | Purpose |
|---|---|
thread::spawn |
Create new thread |
handle.join() |
Wait for thread to finish |
move |
Transfer ownership to closure |
mpsc::channel() |
Create sender/receiver pair |
tx.send() |
Send message (transfers ownership) |
rx.recv() |
Receive message (blocks until arrives) |
Mutex<T> |
Safe mutation (one thread at a time) |
Arc<T> |
Thread-safe shared ownership |
Arc<Mutex<T>> |
Shared + mutable across threads |
Here are practice exercises for Chapter 16:
Exercises for Fearless Concurrency
Exercise 1: Basic Thread Spawning
Create a program that spawns a thread to print "Hello from the spawned thread!" while the main thread prints "Hello from main!".
First, run it without join() a few times and observe what happens.
Then add join() to ensure the spawned thread always completes.
Exercise 2: Moving Data Into Threads
This code won't compile:
use std::thread;
fn main() {
let message = String::from("Hello from the thread!");
let handle = thread::spawn(|| {
println!("{}", message);
});
handle.join().unwrap();
}
- Explain why Rust prevents this
- Fix it using the
movekeyword - After fixing, try to print
messagein main after thejoin()- what happens and why?
Exercise 3: Basic Channel Communication
Create a program where:
- The main thread spawns a worker thread
- The worker thread sends the message "Task completed!" through a channel
- The main thread receives and prints the message
Use mpsc::channel() for this.
Exercise 4: Sending Multiple Messages
Create a program where a spawned thread sends the numbers 1 through 5 through a channel, with a 500ms delay between each send.
The main thread should receive and print each number as it arrives.
Use for received in rx to iterate over incoming messages.
Hint: Use thread::sleep(Duration::from_millis(500)) for the delay.
Exercise 5: Multiple Producers
Create a program with three worker threads, each sending 3 messages to the same channel:
- Thread 1 sends: "A1", "A2", "A3"
- Thread 2 sends: "B1", "B2", "B3"
- Thread 3 sends: "C1", "C2", "C3"
The main thread should receive and print all 9 messages.
Hint: You'll need to clone() the transmitter for each additional thread.
Exercise 6: Shared Counter with Arc<Mutex>
Create a program where 5 threads each increment a shared counter 100 times.
The final result should be 500.
Use Arc<Mutex<i32>> for the shared counter.
Print the final value after all threads complete.
Exercise 7: Parallel Word Counter
You have a list of sentences:
let sentences = vec![
String::from("the quick brown fox"),
String::from("jumps over the lazy dog"),
String::from("the dog barks at the fox"),
];
Create a program that:
- Spawns a thread for each sentence
- Each thread counts the words in its sentence and sends the count through a channel
- The main thread receives all counts and prints the total
Expected output: Total words across all sentences.
Hint: Use .split_whitespace().count() to count words in a string.
Exercise 8: Bank Account Simulation
Create a BankAccount simulation:
- Start with a balance of 1000
- Spawn 3 "deposit" threads that each add 100 to the balance (total +300)
- Spawn 2 "withdraw" threads that each subtract 150 from the balance (total -300)
- Wait for all threads to complete
- Print the final balance (should be 1000)
Use Arc<Mutex<i32>> for the balance.
Bonus: Add a small random delay in each thread using thread::sleep() to simulate real-world timing, and verify the result is still correct.
Exercise 9: Producer-Consumer Pattern
Build a classic producer-consumer system:
- Create one "producer" thread that generates numbers 1 through 10 and sends them through a channel
- Create one "consumer" thread that receives numbers and calculates their sum
- After all numbers are processed, the consumer should send the final sum back to main through a second channel
- Main prints the result (should be 55)
This exercise practices using multiple channels for bidirectional communication.
Exercise 10: Choose the Right Tool
For each scenario below, decide which concurrency tool you would use and explain why:
Scenario A: You need to download 5 files in parallel. Each download is independent and you just need to wait for all to finish.
Scenario B: You have 4 worker threads processing tasks. Each worker needs to report its results back to a main "collector" thread.
Scenario C: You're building a game where multiple threads need to read and update a shared high score.
Scenario D: You have a large configuration object that multiple threads need to read (but never modify).
Choose from:
- Just
thread::spawnwithmove mpsc::channelArc<T>Arc<Mutex<T>>