Rust Fundamentals: Ownership, Errors and Fearless Concurrency · Errors, Collections, Traits and Concurrency

Fearless Concurrency: Threads, Channels and Arc<Mutex>

Run work in parallel with threads, pass messages through channels, and share state safely with Arc and Mutex. The type system rejects data races at compile time.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A parallel word counter: split a text into chunks, count words in several threads, and combine the results, first with channels, then with shared state behind Arc<Mutex<…>>, and finally with scoped threads.

The idea

  • std::thread::spawn runs a closure on a new OS thread and returns a handle; join() waits for it and returns its result. Closures usually need move so the thread owns what it uses.
  • Channels (std::sync::mpsc) pass messages: many senders, one receiver. "Share memory by communicating."
  • Shared state: wrap data in Mutex<T> (one thread at a time) and share the mutex across threads with Arc<T> (an atomically reference-counted pointer).
  • Scoped threads (std::thread::scope) can borrow local data directly, because all threads are guaranteed to finish before the scope ends.

Why "fearless"? Two traits, Send (can be moved to another thread) and Sync (can be shared between threads), are checked by the compiler. Sharing a plain Rc or mutating unsynchronised data across threads simply doesn't compile, so data races can't happen in safe Rust.

For async I/O-heavy servers, Rust uses async/.await with a runtime such as Tokio; threads are the right tool for CPU-bound parallel work.

The code

Parallel word counting three ways
use std::sync::{mpsc, Arc, Mutex};
use std::thread;

fn count_words(chunk: &str) -> usize {
    chunk.split_whitespace().count()
}

fn main() {
    let text = "one two three four five six seven eight nine ten eleven twelve thirteen fourteen";
    let words: Vec<&str> = text.split_whitespace().collect();
    let chunks: Vec<String> = words.chunks(4).map(|c| c.join(" ")).collect();

    // 1. Channels: each worker sends its count to the receiver.
    let (tx, rx) = mpsc::channel();
    for (i, chunk) in chunks.clone().into_iter().enumerate() {
        let tx = tx.clone();
        thread::spawn(move || {
            tx.send((i, count_words(&chunk))).unwrap(); // `move`: the thread owns its chunk
        });
    }
    drop(tx); // close our sender so the loop below ends when workers finish
    let mut results: Vec<(usize, usize)> = rx.iter().collect();
    results.sort(); // threads finish in any order
    println!("channel results: {:?}", results);

    // 2. Shared state: Arc to share, Mutex to take turns.
    let total = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();
    for chunk in chunks.clone() {
        let total = Arc::clone(&total);
        handles.push(thread::spawn(move || {
            let n = count_words(&chunk);
            *total.lock().unwrap() += n;
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("mutex total: {}", *total.lock().unwrap());

    // 3. Scoped threads can borrow `chunks` directly: no Arc, no clones.
    let counts: Vec<usize> = thread::scope(|s| {
        let handles: Vec<_> = chunks.iter().map(|c| s.spawn(move || count_words(c))).collect();
        handles.into_iter().map(|h| h.join().unwrap()).collect()
    });
    println!("scoped counts: {:?}, total {}", counts, counts.iter().sum::<usize>());
}

Output

channel results: [(0, 4), (1, 4), (2, 4), (3, 2)]
mutex total: 14
scoped counts: [4, 4, 4, 2], total 14

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Forgetting move on thread closures, so they try to borrow locals that may not live long enough.
  • Holding a MutexGuard too long (or across slow work), which serialises threads or deadlocks.
  • Using Rc across threads. It isn't Send; use Arc.
  • Expecting a fixed order of results from threads; sort or index them.
  • Threads for I/O-bound servers. Use async (Tokio) for thousands of concurrent connections.

Key points

  • thread::spawn(move || ...) + join(); scoped threads borrow safely.
  • Channels pass ownership between threads; Arc<Mutex<T>> shares mutable state.
  • Send and Sync make data races a compile-time error.
  • CPU-bound work → threads; I/O-bound concurrency → async runtimes.

Further reading: Fearless concurrency.

Practice questions

Why do you need Arc<Mutex<T>> to share mutable data between threads?

Answer

Arc gives shared ownership that is safe across threads (atomic reference counting), and Mutex ensures only one thread accesses the data at a time. Rc isn't thread-safe (not Send), and plain mutable sharing would be a data race, which the compiler rejects.

What are the Send and Sync traits?

Answer

Send means a value can be transferred to another thread; Sync means a reference to it can be shared between threads. The compiler implements them automatically for safe types and refuses to compile code that would move or share non-Send/non-Sync values across threads.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?