Fearless Concurrency in Rust

January 1, 2026

Part 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:

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:

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).

.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) │
└─────────────────┘                      └─────────────────┘

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:


mpsc::channel()

Creates a channel. Returns a tuple of two things:


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>:

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>:


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:

  1. Calls recv() for each iteration
  2. 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:

The Danger: Data Races

A data race happens when:

  1. Two threads access the same data
  2. At least one is writing
  3. 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:

  1. Before entering, you lock the door
  2. While you're inside, no one else can enter
  3. When done, you unlock the door
  4. 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:

Returns Result<MutexGuard, PoisonError>.


let mut num = counter.lock().unwrap();

num is a MutexGuard - a smart pointer that:


*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

  1. Always lock in the same order - if everyone locks X before Y, no circular waiting
  2. Hold locks briefly - lock, do work, unlock quickly
  3. 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();
}
  1. Explain why Rust prevents this
  2. Fix it using the move keyword
  3. After fixing, try to print message in main after the join() - what happens and why?

Exercise 3: Basic Channel Communication

Create a program where:

  1. The main thread spawns a worker thread
  2. The worker thread sends the message "Task completed!" through a channel
  3. 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:

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:

  1. Spawns a thread for each sentence
  2. Each thread counts the words in its sentence and sends the count through a channel
  3. 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:

  1. Start with a balance of 1000
  2. Spawn 3 "deposit" threads that each add 100 to the balance (total +300)
  3. Spawn 2 "withdraw" threads that each subtract 150 from the balance (total -300)
  4. Wait for all threads to complete
  5. 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:

  1. Create one "producer" thread that generates numbers 1 through 10 and sends them through a channel
  2. Create one "consumer" thread that receives numbers and calculates their sum
  3. After all numbers are processed, the consumer should send the final sum back to main through a second channel
  4. 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:

  1. Scenario A: You need to download 5 files in parallel. Each download is independent and you just need to wait for all to finish.

  2. Scenario B: You have 4 worker threads processing tasks. Each worker needs to report its results back to a main "collector" thread.

  3. Scenario C: You're building a game where multiple threads need to read and update a shared high score.

  4. Scenario D: You have a large configuration object that multiple threads need to read (but never modify).

Choose from: