100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Rust

Concurrency in Rust (Threads, Channels, Mutex)

Learn how Rust's ownership rules enable fearless concurrency using threads, channels, and mutexes with compile-time data race prevention.

Concurrency & ToolingIntermediate10 min readJul 8, 2026
Analogies

Introduction

Concurrency lets a program do multiple things at once, but it is notoriously hard to get right because of data races, deadlocks, and shared mutable state. Rust tackles this problem with what its creators call 'fearless concurrency': the same ownership and borrowing rules that prevent memory bugs also prevent data races, and the compiler catches most concurrency mistakes before the program ever runs. Rust offers OS threads via std::thread, message passing via channels, and shared-state concurrency via Mutex and Arc.

🏏

Cricket analogy: Running two net sessions at once is risky if two bowlers grab the same ball (data race) or two players wait on each other's turn forever (deadlock); Rust's fearless concurrency is like a ground with strict queueing rules enforced by an umpire, dedicated lanes (threads), a runner relaying scores (channels), and a single shared kit bag with one key (Mutex+Arc).

Syntax

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

// Spawn a thread
let handle = thread::spawn(move || {
    // work done on a new OS thread
});
handle.join().unwrap();

// Channel for message passing
let (tx, rx) = mpsc::channel();
tx.send(42).unwrap();
let received = rx.recv().unwrap();

// Shared mutable state
let counter = Arc::new(Mutex::new(0));

Explanation

thread::spawn takes a closure and runs it on a new OS thread, returning a JoinHandle; calling .join() blocks the current thread until the spawned thread finishes. Because the spawned thread may outlive the data it borrows, closures passed to spawn are usually written with the move keyword so they take ownership of any captured variables instead of borrowing them. For communication between threads, std::sync::mpsc::channel() creates a multi-producer, single-consumer channel: tx.send(value) pushes a value and rx.recv() blocks until one arrives. When threads need to share and mutate the same data, Mutex<T> wraps the data and only allows one thread at a time to access it through the guard returned by .lock(); to share ownership of that Mutex across threads you wrap it in Arc<T>, an atomically reference-counted pointer. Rc<T> is not safe to share across threads because its reference count is not updated atomically, so Rust requires Arc<T> instead. Under the hood, the Send and Sync marker traits tell the compiler which types are safe to move or share between threads, and it refuses to compile code that violates them, catching data races at compile time.

🏏

Cricket analogy: thread::spawn is like sending a fielder off to warm up in a separate net (new OS thread) with .join() being the captain waiting for that fielder to report back before play resumes; the mpsc channel is like a runner relaying scores from the boundary to the pavilion (tx.send/rx.recv), and Arc<Mutex<T>> is like a shared team scorebook only one player may write in at a time, safely tracked among all players (unlike Rc, which can't be shared across separate grounds).

Example

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..5 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Output

text
Result: 5

Key Takeaways

  • thread::spawn creates an OS thread and returns a JoinHandle; call .join() to wait for it to finish.
  • Use move closures to transfer ownership of captured variables into a spawned thread.
  • mpsc::channel() provides multi-producer, single-consumer message passing via send/recv.
  • Mutex<T> guards shared mutable state; Arc<T> shares ownership of that Mutex across threads (Rc<T> is not thread-safe).
  • The Send and Sync traits let the compiler catch data races at compile time — this is 'fearless concurrency'.

Practice what you learned

Was this page helpful?

Topics covered

#Rust#RustProgrammingStudyNotes#Programming#ConcurrencyInRustThreadsChannelsMutex#Concurrency#Threads#Channels#Mutex#StudyNotes#SkillVeris