TechLead
Lesson 13 of 28
5 min read
Rust

Fearless Concurrency

Learn Rust concurrency with threads, message passing via channels, shared state with Mutex and Arc, and the Send and Sync traits.

Why "Fearless" Concurrency?

Rust's ownership system prevents data races at compile time. In languages like C++ or Java, data races are runtime bugs that are notoriously hard to reproduce and fix. In Rust, the compiler simply won't let you write code that has data races. This is what makes Rust concurrency "fearless."

Rust's Concurrency Guarantees

  • No Data Races: The type system prevents simultaneous mutable access from multiple threads
  • Send Trait: Types that can be transferred between threads
  • Sync Trait: Types that can be shared between threads via references

Threads with std::thread

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

fn main() {
    // Spawn a thread
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("Spawned thread: {i}");
            thread::sleep(Duration::from_millis(10));
        }
    });

    for i in 1..3 {
        println!("Main thread: {i}");
        thread::sleep(Duration::from_millis(10));
    }

    // Wait for the spawned thread to finish
    handle.join().unwrap();
    println!("Both threads done!");

    // Move data into a thread
    let data = vec![1, 2, 3];
    let handle = thread::spawn(move || {
        // move keyword transfers ownership to the thread
        println!("Thread got: {:?}", data);
        let sum: i32 = data.iter().sum();
        sum // Return value from thread
    });

    // data is no longer available here — it was moved
    let result = handle.join().unwrap();
    println!("Thread returned: {result}");

    // Spawn multiple threads
    let mut handles = vec![];
    for i in 0..5 {
        handles.push(thread::spawn(move || {
            println!("Thread {i} running");
            i * i
        }));
    }

    let results: Vec = handles
        .into_iter()
        .map(|h| h.join().unwrap())
        .collect();
    println!("Results: {:?}", results);
}

Message Passing with Channels

use std::sync::mpsc; // Multiple Producer, Single Consumer
use std::thread;

fn main() {
    // Create a channel
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let messages = vec![
            String::from("hello"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];
        for msg in messages {
            tx.send(msg).unwrap();
            thread::sleep(std::time::Duration::from_millis(100));
        }
        // tx is dropped here — channel closes
    });

    // Receive messages (blocks until message arrives)
    for received in rx {
        println!("Got: {received}");
    }

    // Multiple producers
    let (tx, rx) = mpsc::channel();
    let tx2 = tx.clone(); // Clone the sender

    thread::spawn(move || {
        tx.send("from thread 1").unwrap();
    });

    thread::spawn(move || {
        tx2.send("from thread 2").unwrap();
    });

    for msg in rx {
        println!("Received: {msg}");
    }
}

Shared State with Mutex<T> and Arc<T>

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

fn main() {
    // Mutex provides interior mutability with locking
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // lock() blocks until the lock is available
            let mut num = counter.lock().unwrap();
            *num += 1;
            // Lock is automatically released when num goes out of scope
        });
        handles.push(handle);
    }

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

    println!("Counter: {}", *counter.lock().unwrap()); // 10

    // More complex shared state
    let data = Arc::new(Mutex::new(Vec::new()));
    let mut handles = vec![];

    for i in 0..5 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let mut vec = data.lock().unwrap();
            vec.push(i);
        }));
    }

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

    let result = data.lock().unwrap();
    println!("Shared vec: {:?}", *result);
}

Send and Sync Traits

// Send: ownership can be transferred between threads
// - Most types are Send
// - Rc is NOT Send (use Arc instead)
// - Raw pointers are NOT Send

// Sync: can be referenced from multiple threads
// - T is Sync if &T is Send
// - Mutex is Sync (that's its purpose!)
// - RefCell is NOT Sync (use Mutex instead)
// - Rc is NOT Sync (use Arc instead)

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

// The compiler enforces Send + Sync automatically!
// This won't compile because Rc is not Send:

// use std::rc::Rc;
// let data = Rc::new(5);
// thread::spawn(move || {
//     println!("{}", data); // ERROR: Rc cannot be sent between threads
// });

// Fix: use Arc instead
fn thread_safe_example() {
    let data = Arc::new(5);
    let data_clone = Arc::clone(&data);
    let handle = thread::spawn(move || {
        println!("Thread sees: {}", data_clone); // OK!
    });
    handle.join().unwrap();
    println!("Main sees: {}", data);
}

Avoiding Deadlocks

Rust prevents data races but not deadlocks. Always acquire locks in a consistent order, minimize lock scope, and prefer message passing (channels) over shared state when possible.

Key Takeaways

  • ✅ Rust prevents data races at compile time through ownership and the type system
  • ✅ Use channels (mpsc) for message-passing concurrency between threads
  • ✅ Combine Arc<T> and Mutex<T> for shared mutable state across threads
  • Send and Sync traits are automatically derived by the compiler
  • ✅ Prefer channels over shared state when possible for simpler concurrent code

Continue Learning