Frontend Labs
⋯

29. Shared state: Mutex, RwLock, atomics, Condvar, channels

29.1 Mutex<T> owns the data it protects

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

fn main() {
    let hits: Arc<Mutex<HashMap<String, u32>>> = Arc::new(Mutex::new(HashMap::new()));

    let workers: Vec<_> = ["/a", "/b", "/a", "/c", "/a"]
        .into_iter()
        .map(|path| {
            let hits = Arc::clone(&hits);            // one Arc handle per thread
            thread::spawn(move || {
                let mut map = hits.lock().unwrap();  // MutexGuard<HashMap<..>>
                *map.entry(path.to_string()).or_insert(0) += 1;
            })                                       // guard dropped here: unlocked
        })
        .collect();
    for w in workers { w.join().unwrap(); }

    let map = hits.lock().unwrap();
    println!("{:?}", map.get("/a")); // Some(3)
}

In C and Go, a mutex protects whatever the programmer remembers to access only while holding it. In Rust, Mutex<T> contains the T. The only way to reach the data is lock(), which returns a guard, and the guard unlocks when it is dropped. You cannot forget to lock, and you cannot forget to unlock.

Poisoning: if a thread panics while holding the lock, the mutex is marked poisoned and lock() returns Err. The data might be half-updated. .unwrap() turns that into a panic. Use .unwrap_or_else(|e| e.into_inner()) if you know the data is still consistent.

29.2 What Rust does not prevent: deadlocks and logical races

// fragment
// Thread 1                       // Thread 2
let a = accounts.lock();          let b = audit.lock();
let b = audit.lock();   // waits  let a = accounts.lock();   // waits -> DEADLOCK

Deadlocks are memory-safe, so the compiler allows them. Use the same disciplines as the OS guide Ch 18: a global lock order, holding locks for short scopes, and never calling unknown code (callbacks, .await) while holding a lock. Check-then-act races are also possible: if map.lock().contains(k) { map.lock().insert(..) } takes the lock twice, and another thread can insert in between. Do the whole operation under one guard.

29.3 RwLock and atomics

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, RwLock};
use std::thread;

struct Metrics {
    requests: AtomicU64,                 // lock-free counter
    config: RwLock<String>,              // many readers OR one writer
}

fn main() {
    let m = Arc::new(Metrics { requests: AtomicU64::new(0), config: RwLock::new("v1".into()) });
    thread::scope(|s| {
        for _ in 0..8 {
            s.spawn(|| {
                for _ in 0..10_000 {
                    m.requests.fetch_add(1, Ordering::Relaxed); // a counter needs no ordering (Ch 61)
                    let _cfg = m.config.read().unwrap();         // readers do not block each other
                }
            });
        }
        s.spawn(|| *m.config.write().unwrap() = "v2".into());
    });
    println!("{} requests, config {}", m.requests.load(Ordering::Relaxed), m.config.read().unwrap());
}

29.4 Channels: "share memory by communicating", Rust edition

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

fn main() {
    // sync_channel(N) is BOUNDED: senders block when the buffer is full (backpressure).
    let (tx, rx) = mpsc::sync_channel::<(usize, String)>(16);

    for id in 0..3 {
        let tx = tx.clone();                       // multi-producer
        thread::spawn(move || {
            for j in 0..2 {
                tx.send((id, format!("job {j}"))).unwrap();
                thread::sleep(Duration::from_millis(5));
            }
        });                                        // this thread's tx is dropped here
    }
    drop(tx); // drop the original sender, or the receiver loop below never ends

    for (id, msg) in rx {                          // ends when ALL senders are dropped
        println!("worker {id}: {msg}");
    }
}
Go Rust (std) Rust (Tokio, Ch 31)
make(chan T) (unbuffered) sync_channel(0) —
make(chan T, n) sync_channel(n) tokio::sync::mpsc::channel(n)
close(ch) drop every Sender drop every Sender
select — (use crossbeam-channel) tokio::select!
broadcast — tokio::sync::broadcast
"latest value" — tokio::sync::watch

Sending a value moves it. After tx.send(buf), the sending thread can no longer touch buf. In Go, sending a pointer or slice over a channel and then mutating it is a common race. In Rust that is a compile error.

29.5 Condvar: waiting for a condition

Condvar works like pthread_cond_t in the C guide's producer/consumer example: wait while holding a MutexGuard and loop on the condition, because wakeups can be spurious. Most code should use a channel instead. Ch 37 uses one inside a thread pool.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36