Learn › Rust Programming

Concurrency

Explore Rust's fearless concurrency with threads, channels, Arc, and Mutex.

Spawning Threads

Rust provides native operating system threads through the std::thread module. The thread::spawn function takes a closure and runs it in a new thread, returning a JoinHandle that you can use to wait for the thread to finish. The move keyword transfers ownership of captured variables into the thread's closure, which is often necessary because the thread may outlive the scope that created it. Rust's ownership system ensures at compile time that data races between threads cannot occur.

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

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

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

    // Wait for thread and get its return value
    let result = handle.join().unwrap();
    println!("Thread returned: {}", result);

    // Move ownership into thread
    let data = vec![1, 2, 3, 4, 5];
    let handle = thread::spawn(move || {
        let sum: i32 = data.iter().sum();
        println!("Sum from thread: {}", sum);
    });
    handle.join().unwrap();
    // data is no longer accessible here - it was moved
}

Channels for Message Passing

Channels provide a way for threads to communicate by sending messages. Rust's standard library offers a multi-producer, single-consumer channel through mpsc::channel. The transmitter can be cloned to allow multiple threads to send messages to the same receiver. Channels transfer ownership of the sent value to the receiver, which naturally prevents data races. This message-passing style of concurrency follows the philosophy of sharing memory by communicating rather than communicating by sharing memory.

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

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

    // Clone transmitter for multiple producers
    let tx2 = tx.clone();

    thread::spawn(move || {
        let messages = vec!["hello", "from", "thread", "one"];
        for msg in messages {
            tx.send(format!("[T1] {}", msg)).unwrap();
            thread::sleep(Duration::from_millis(100));
        }
    });

    thread::spawn(move || {
        let messages = vec!["greetings", "from", "thread", "two"];
        for msg in messages {
            tx2.send(format!("[T2] {}", msg)).unwrap();
            thread::sleep(Duration::from_millis(150));
        }
    });

    // Receive all messages
    for received in rx {
        println!("Got: {}", received);
    }

    println!("All messages received.");
}

Shared State with Arc

Arc, which stands for Atomic Reference Counted, is a thread-safe reference-counting pointer that allows multiple threads to share ownership of the same data. Unlike Rc which is only safe for single-threaded use, Arc uses atomic operations for its reference count so it can be safely shared across threads. Arc is immutable by default, so it is typically combined with a Mutex or RwLock to allow shared mutable access. The clone method creates a new pointer to the same data and increments the reference count.

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
    let mut handles = vec![];

    // Share read-only data across threads
    for chunk_id in 0..3 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let start = chunk_id * 3;
            let end = (start + 3).min(data.len());
            let sum: i32 = data[start..end].iter().sum();
            println!(
                "Thread {}: sum of {:?} = {}",
                chunk_id,
                &data[start..end],
                sum
            );
            sum
        });
        handles.push(handle);
    }

    let total: i32 = handles
        .into_iter()
        .map(|h| h.join().unwrap())
        .sum();

    println!("Total sum: {}", total);
    println!("Reference count: {}", Arc::strong_count(&data));
}

Mutex for Mutual Exclusion

A Mutex provides mutual exclusion, ensuring that only one thread can access the protected data at a time. To read or write the data you call lock, which blocks until the lock is available and returns a guard that automatically releases the lock when it goes out of scope. Combining Arc with Mutex gives you a thread-safe, shared, mutable value. Rust's type system prevents you from accessing the data without locking the mutex, which eliminates an entire class of concurrency bugs at compile time.

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

fn main() {
    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 || {
            for _ in 0..100 {
                let mut num = counter.lock().unwrap();
                *num += 1;
            }
        });
        handles.push(handle);
    }

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

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

    // Shared mutable collection
    let results = Arc::new(Mutex::new(Vec::new()));
    let mut handles = vec![];

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

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

    let final_results = results.lock().unwrap();
    println!("Results: {:?}", *final_results);
}

← Collections