Lesson 11 of 12

Concurrency & Smart Pointers

Learn how to run code on multiple threads and share data safely with smart . Rust's ownership rules turn data races into compile .

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

fn main() {
    // Threads: move takes ownership of what the closure captures
    let data = vec![1, 2, 3];
    let handle = thread::spawn(move || {
        println!("{:?}", data);
        data.iter().sum::<i32>()
    });
    let sum = handle.join().unwrap();     // join returns the closure's value
    println!("sum = {sum}");

    // Channels: multiple producers, single consumer
    let (tx, rx) = mpsc::channel();
    for id in 0..3 {
        let tx = tx.clone();
        thread::spawn(move || {
            thread::sleep(Duration::from_millis(50 * id));
            tx.send(format!("worker {id} done")).unwrap();
        });
    }
    drop(tx);                              // drop the last sender so rx can finish

    for msg in rx {                        // iterates until every sender is gone
        println!("{msg}");
    }
}
Rust
use std::sync::{Arc, Mutex, RwLock};
use std::thread;

fn main() {
    // Arc = Atomically Reference Counted: shared ownership across threads.
    // Mutex = mutual exclusion: one writer at a time.
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..8 {
        let counter = Arc::clone(&counter);     // clones the HANDLE, not the data
        handles.push(thread::spawn(move || {
            let mut n = counter.lock().unwrap(); // blocks until the lock is free
            *n += 1;
        }));                                     // lock released here, automatically
    }

    for h in handles { h.join().unwrap(); }
    println!("{}", *counter.lock().unwrap());    // always 8 — no race possible

    // RwLock: many readers OR one writer
    let cache = Arc::new(RwLock::new(vec!["a".to_string()]));
    {
        let read = cache.read().unwrap();
        println!("{}", read.len());
    }
    cache.write().unwrap().push("b".into());

    // Rc<RefCell<T>> is the single-threaded equivalent (Rc is not thread safe).
}
Rust
use std::rc::Rc;
use std::cell::RefCell;

// Box<T>: a value on the heap. Needed for recursive types of unknown size.
#[derive(Debug)]
enum Tree {
    Leaf(i32),
    Node(Box<Tree>, Box<Tree>),
}

fn sum(tree: &Tree) -> i32 {
    match tree {
        Tree::Leaf(v) => *v,
        Tree::Node(l, r) => sum(l) + sum(r),
    }
}

// Rc<T>: shared ownership in one thread. RefCell<T>: borrow rules checked at RUNTIME.
#[derive(Debug)]
struct Account { balance: RefCell<f64> }

fn main() {
    let tree = Tree::Node(
        Box::new(Tree::Leaf(1)),
        Box::new(Tree::Node(Box::new(Tree::Leaf(2)), Box::new(Tree::Leaf(3)))),
    );
    println!("{}", sum(&tree));               // 6

    let account = Rc::new(Account { balance: RefCell::new(100.0) });
    let a = Rc::clone(&account);
    let b = Rc::clone(&account);

    *a.balance.borrow_mut() += 50.0;          // mutate through a shared reference
    *b.balance.borrow_mut() -= 20.0;

    println!("{} owners, balance {}", Rc::strong_count(&account), account.balance.borrow());
}
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.