← Back to Rust series
πŸ¦€
Modern Rust
Modern Β· Prerequisite: lesson 20

21. Threads, Channels, Arc/Mutex

Rust calls its model "fearless concurrency" because the borrow checker prevents data races at compile time even across threads. This lesson covers std::thread, mpsc channels for thread-to-thread messages, and the Arc<Mutex<T>> pattern when you actually need shared state.

RustthreadconcurrencyArcMutexchannelmpsc
Duration
⏱ ~2 hours
Level
πŸ“Š Intermediate
Prerequisite
🎯 Lesson 20
OUTCOME
Rust calls its model "fearless concurrency" because the borrow checker prevents data races at compile time even across threads. This lesson covers std::thread, mpsc channels for thread-to-thread messages, and the Arc<Mutex<T>> pattern when you actually need shared state.

What you'll learn

  • 1Spawn threads with `std::thread::spawn` and join them
  • 2Move values into a thread via a `move` closure
  • 3Pass data between threads with mpsc channels
  • 4Mutate shared state safely with `Arc<Mutex<T>>`
  • 5Understand the role of the Send / Sync traits

Overview

Rust's concurrency promise is **"safe even when you make mistakes."** The same borrowing rule that prevented data races in single-threaded code scales out to threads, so the compiler catches missing locks before runtime. Debugging time goes way down.

Core Concepts

1) Spawning a thread

rust
use std::thread;
use std::time::Duration;

fn main() {
    let h = thread::spawn(|| {
        for i in 1..=3 {
            println!("child {}", i);
            thread::sleep(Duration::from_millis(50));
        }
    });
    for i in 1..=3 {
        println!("main {}", i);
        thread::sleep(Duration::from_millis(50));
    }
    h.join().unwrap();
}

2) move closures to transfer values

If a closure borrows from outside, the compiler can't prove the thread won't outlive the data. `move` transfers ownership in.

rust
let v = vec![1, 2, 3];
let h = thread::spawn(move || println!("{:?}", v));
// v has moved into the thread
h.join().unwrap();

3) mpsc channels β€” message passing

Standard multi-producer, single-consumer channel. Multiple senders, one receiver.

4) Arc<Mutex<T>> for shared mutable state

  • **Arc<T>** β€” atomic reference count, shareable across threads
  • **Mutex<T>** β€” only one thread can write at a time (runtime lock)
  • **Arc<Mutex<T>>** combo β€” multiple threads share + mutate safely

5) Send / Sync traits

TraitMeaning
SendSafe to transfer ownership across threads
SyncSafe to share references across threads

Auto-derived for nearly all standard types. Rc and RefCell are **not** Send/Sync β€” passing one across threads is a compile error, and Arc/Mutex are the substitute.

Hands-on Examples

Send 1~10 from a worker, sum in main:

rust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel::<i32>();
    thread::spawn(move || {
        for i in 1..=10 { tx.send(i).unwrap(); }
    });
    let sum: i32 = rx.iter().sum();
    println!("sum = {}", sum); // 55
}

Increment a counter safely from many threads:

rust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut hs = vec![];
    for _ in 0..10 {
        let c = Arc::clone(&counter);
        hs.push(thread::spawn(move || {
            let mut n = c.lock().unwrap();
            *n += 1;
        }));
    }
    for h in hs { h.join().unwrap(); }
    println!("counter = {}", *counter.lock().unwrap()); // 10
}

Common Mistakes

Q. Can I pass Rc across threads?

A. No β€” Rc's reference count is non-atomic and races. Use Arc instead (atomic). The compiler enforces this via Send.

Q. When does a Mutex unlock?

A. When the **MutexGuard you got from `lock()` is dropped** β€” usually at end of scope. To release earlier use `drop(guard)` or narrow the scope with an explicit block.

Q. async/await as a replacement?

A. CPU work fits threads; IO-bound work fits async. The next lesson covers async I/O.

Recap

  • Spawn with `thread::spawn` + move closure
  • Pass data via mpsc channels
  • Shared mutable state = Arc<Mutex<T>>
  • Send/Sync traits encode thread safety at compile time

Try It Yourself

  1. Four threads sum 1..25, 26..50, 51..75, 76..100; aggregate via mpsc in main
  2. Make `Arc<Mutex<Vec<i32>>>` and have several threads push into it safely
  3. Try passing Rc into `thread::spawn` to read the exact compiler error
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—