blog.dopana

Back

Fifteenth post in the intermediate Rust series. Concurrent programming has traditionally been difficult and error-prone — data races, deadlocks, and memory corruption are notorious bugs in multi-threaded systems.

Rust tackles this with the concept of Fearless Concurrency: ownership and the type system turn concurrency bugs into compile-time errors rather than runtime nightmares.

graph TD
    A["Fearless Concurrency in Rust"] --> B["Thread Spawning<br/>std::thread::spawn"]
    A --> C["Message Passing<br/>Channels (mpsc)"]
    A --> D["Shared State<br/>Arc<T> + Mutex<T>"]
    A --> E["Extensible Safety<br/>Send & Sync Traits"]

Explain Like I’m 10: The Busy Restaurant Kitchen#

Imagine a busy restaurant kitchen:

  1. Single-threaded: Only 1 chef does everything from chopping vegetables to cooking and dishwashing. Very safe, but slow.
  2. Unsafe multi-threading (C/C++): 5 chefs grab the same chopping board with no rules, accidentally cutting each other and mixing up ingredients (race conditions, data corruption).
  3. Rust Fearless Concurrency: Each chef has their own designated station (ownership), communicates via order conveyor belts (channels), and if they must share a rare copper pan, they lock and unlock it with a key (Arc + Mutex). The head chef (the compiler) checks all safety protocols before the kitchen even opens!

1. Using Threads to Run Code Simultaneously#

In Rust, you spawn native OS threads with thread::spawn:

Transferring Ownership with move Closures#

When a spawned thread needs data from the surrounding scope, use move to transfer ownership to the thread:

src/main.rs
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Vector inside thread: {v:?}");
    });

    handle.join().unwrap();
}
rust

2. Message Passing: Channels (mpsc)#

A popular motto from the Go community rings equally true in Rust: “Do not communicate by sharing memory; instead, share memory by communicating.”

Rust provides mpsc (multiple producer, single consumer) channels in the standard library:

3. Shared-State Concurrency: Arc<T> and Mutex<T>#

When threads need to access and mutate the same shared data:

  • Mutex<T> (Mutual Exclusion): Guards access so only one thread can hold the lock and access the inner value at a time.
  • Arc<T> (Atomic Reference Counting): A thread-safe smart pointer that allows multiple threads to share ownership of the Mutex.

[!NOTE] Rc<T> is not thread-safe because its reference count is not updated atomically. The Rust compiler will refuse to compile code that sends Rc<T> across threads, enforcing the use of Arc<T>.

4. Extensible Safety: Send and Sync Traits#

Rust’s concurrency guarantees are built on two fundamental auto traits:

  1. Send: Indicates that ownership of the type can be transferred between threads.
  2. Sync: Indicates that it is safe for multiple threads to access references (&T) to the type simultaneously (T: Sync implies &T: Send).

Most Rust primitives are automatically Send and Sync. Types with non-thread-safe internal state (like Rc<T> or RefCell<T>) are marked !Send or !Sync, preventing unsafe multi-threaded usage at compile time.

Summary#

  • thread::spawn creates OS threads with safe ownership transfer via move closures.
  • mpsc::channel offers safe message passing between multiple producers and a single consumer.
  • Arc<Mutex<T>> is the standard pattern for shared, mutable state across multiple threads.
  • Send and Sync traits empower the compiler to enforce data race prevention at compile time.

References#