ExamplesBy LevelBy TopicLearning Paths
986 Fundamental

986 Mutex Basics

Functional Programming

Tutorial

The Problem

Use Mutex<T> to protect shared mutable state across threads. Combine with Arc<T> for shared ownership. Demonstrate RAII-based lock acquisition (the guard drops automatically at end of scope), shared counter increment from 10 threads, and a mutex-protected BankAccount struct for structured state.

🎯 Learning Outcomes

  • • Combine Arc<Mutex<T>> for shared mutable state: Arc::clone shares the pointer, Mutex::lock protects access
  • • Understand that Mutex::lock returns MutexGuard<T> — a RAII guard that releases the lock when dropped
  • • Write *n += 1 to increment through the guard's DerefMut implementation
  • • Avoid deadlock by keeping lock scopes short: lock, mutate, drop guard, not lock across blocking operations
  • • Understand the OCaml equivalent: Mutex.lock / Mutex.unlock or Mutex.protect
  • Code Example

    #![allow(clippy::all)]
    // 986: Mutex-Protected State
    // Rust: Mutex<T> owns the data — unlocks automatically via RAII guard
    
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    // --- Approach 1: Shared counter with Arc<Mutex<i32>> ---
    fn shared_counter() -> i32 {
        let counter = Arc::new(Mutex::new(0i32));
    
        let handles: Vec<_> = (0..10)
            .map(|_| {
                let counter = Arc::clone(&counter);
                thread::spawn(move || {
                    for _ in 0..100 {
                        let mut n = counter.lock().unwrap();
                        *n += 1;
                        // Lock released here when `n` drops
                    }
                })
            })
            .collect();
    
        for h in handles {
            h.join().unwrap();
        }
        let x = *counter.lock().unwrap();
        x
    }
    
    // --- Approach 2: Mutex around structured state ---
    #[derive(Debug)]
    struct BankAccount {
        balance: i64,
        transactions: u32,
    }
    
    impl BankAccount {
        fn new() -> Self {
            BankAccount {
                balance: 0,
                transactions: 0,
            }
        }
    
        fn deposit(&mut self, amount: i64) {
            self.balance += amount;
            self.transactions += 1;
        }
    
        fn withdraw(&mut self, amount: i64) -> bool {
            if self.balance >= amount {
                self.balance -= amount;
                self.transactions += 1;
                true
            } else {
                false
            }
        }
    }
    
    fn bank_account_demo() -> (i64, u32) {
        let account = Arc::new(Mutex::new(BankAccount::new()));
    
        let handles: Vec<_> = (0..5)
            .map(|_| {
                let acct = Arc::clone(&account);
                thread::spawn(move || {
                    acct.lock().unwrap().deposit(100);
                })
            })
            .collect();
    
        for h in handles {
            h.join().unwrap();
        }
    
        let mut acct = account.lock().unwrap();
        acct.withdraw(200);
        (acct.balance, acct.transactions)
    }
    
    // --- Approach 3: with_mutex helper (bracket / RAII equivalent) ---
    fn with_lock<T, R, F: FnOnce(&mut T) -> R>(m: &Mutex<T>, f: F) -> R {
        let mut guard = m.lock().unwrap();
        f(&mut *guard)
        // guard drops here — unlock is automatic
    }
    
    fn collect_to_vec() -> Vec<i32> {
        let shared = Arc::new(Mutex::new(Vec::<i32>::new()));
    
        let handles: Vec<_> = (0..5i32)
            .map(|i| {
                let shared = Arc::clone(&shared);
                thread::spawn(move || {
                    with_lock(&shared, |v| v.push(i));
                })
            })
            .collect();
    
        for h in handles {
            h.join().unwrap();
        }
    
        let mut v = shared.lock().unwrap().clone();
        v.sort();
        v
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_shared_counter() {
            assert_eq!(shared_counter(), 1000);
        }
    
        #[test]
        fn test_bank_account() {
            let (balance, txns) = bank_account_demo();
            assert_eq!(balance, 300); // 5*100 - 200
            assert_eq!(txns, 6); // 5 deposits + 1 withdrawal
        }
    
        #[test]
        fn test_collect_to_vec() {
            let v = collect_to_vec();
            assert_eq!(v, vec![0, 1, 2, 3, 4]);
        }
    
        #[test]
        fn test_mutex_protects_from_data_race() {
            // If counter were unprotected, this would be UB/wrong
            assert_eq!(shared_counter(), 1000);
        }
    
        #[test]
        fn test_with_lock_helper() {
            let m = Mutex::new(0i32);
            with_lock(&m, |v| *v += 1);
            with_lock(&m, |v| *v += 1);
            assert_eq!(*m.lock().unwrap(), 2);
        }
    }

    Key Differences

    AspectRustOCaml
    Lock scopeRAII guard — automatic releaseManual lock/unlock or Mutex.protect
    Shared ownershipArc<Mutex<T>>Mutex.t + ref (shared implicitly)
    PoisoningMutex poisons on thread panicNo equivalent — mutex stays usable
    Data ownershipMutex owns TMutex and data are separate

    Rust's Mutex<T> is unique: the lock and the data it protects are the same object. You cannot access the data without holding the lock — the type system enforces this invariant.

    OCaml Approach

    let shared_counter () =
      let m = Mutex.create () in
      let counter = ref 0 in
    
      let threads = List.init 10 (fun _ ->
        Thread.create (fun () ->
          for _ = 1 to 100 do
            Mutex.lock m;
            incr counter;
            Mutex.unlock m
          done
        ) ()
      ) in
    
      List.iter Thread.join threads;
      !counter
    
    (* Safer with protect *)
    let with_mutex m f =
      Mutex.lock m;
      match f () with
      | v -> Mutex.unlock m; v
      | exception e -> Mutex.unlock m; raise e
    

    OCaml's Mutex.lock / Mutex.unlock are explicit. with_mutex wraps them with exception safety — analogous to Rust's RAII guard. OCaml 5.0+ uses Mutex.protect f as the built-in equivalent.

    Full Source

    #![allow(clippy::all)]
    // 986: Mutex-Protected State
    // Rust: Mutex<T> owns the data — unlocks automatically via RAII guard
    
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    // --- Approach 1: Shared counter with Arc<Mutex<i32>> ---
    fn shared_counter() -> i32 {
        let counter = Arc::new(Mutex::new(0i32));
    
        let handles: Vec<_> = (0..10)
            .map(|_| {
                let counter = Arc::clone(&counter);
                thread::spawn(move || {
                    for _ in 0..100 {
                        let mut n = counter.lock().unwrap();
                        *n += 1;
                        // Lock released here when `n` drops
                    }
                })
            })
            .collect();
    
        for h in handles {
            h.join().unwrap();
        }
        let x = *counter.lock().unwrap();
        x
    }
    
    // --- Approach 2: Mutex around structured state ---
    #[derive(Debug)]
    struct BankAccount {
        balance: i64,
        transactions: u32,
    }
    
    impl BankAccount {
        fn new() -> Self {
            BankAccount {
                balance: 0,
                transactions: 0,
            }
        }
    
        fn deposit(&mut self, amount: i64) {
            self.balance += amount;
            self.transactions += 1;
        }
    
        fn withdraw(&mut self, amount: i64) -> bool {
            if self.balance >= amount {
                self.balance -= amount;
                self.transactions += 1;
                true
            } else {
                false
            }
        }
    }
    
    fn bank_account_demo() -> (i64, u32) {
        let account = Arc::new(Mutex::new(BankAccount::new()));
    
        let handles: Vec<_> = (0..5)
            .map(|_| {
                let acct = Arc::clone(&account);
                thread::spawn(move || {
                    acct.lock().unwrap().deposit(100);
                })
            })
            .collect();
    
        for h in handles {
            h.join().unwrap();
        }
    
        let mut acct = account.lock().unwrap();
        acct.withdraw(200);
        (acct.balance, acct.transactions)
    }
    
    // --- Approach 3: with_mutex helper (bracket / RAII equivalent) ---
    fn with_lock<T, R, F: FnOnce(&mut T) -> R>(m: &Mutex<T>, f: F) -> R {
        let mut guard = m.lock().unwrap();
        f(&mut *guard)
        // guard drops here — unlock is automatic
    }
    
    fn collect_to_vec() -> Vec<i32> {
        let shared = Arc::new(Mutex::new(Vec::<i32>::new()));
    
        let handles: Vec<_> = (0..5i32)
            .map(|i| {
                let shared = Arc::clone(&shared);
                thread::spawn(move || {
                    with_lock(&shared, |v| v.push(i));
                })
            })
            .collect();
    
        for h in handles {
            h.join().unwrap();
        }
    
        let mut v = shared.lock().unwrap().clone();
        v.sort();
        v
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_shared_counter() {
            assert_eq!(shared_counter(), 1000);
        }
    
        #[test]
        fn test_bank_account() {
            let (balance, txns) = bank_account_demo();
            assert_eq!(balance, 300); // 5*100 - 200
            assert_eq!(txns, 6); // 5 deposits + 1 withdrawal
        }
    
        #[test]
        fn test_collect_to_vec() {
            let v = collect_to_vec();
            assert_eq!(v, vec![0, 1, 2, 3, 4]);
        }
    
        #[test]
        fn test_mutex_protects_from_data_race() {
            // If counter were unprotected, this would be UB/wrong
            assert_eq!(shared_counter(), 1000);
        }
    
        #[test]
        fn test_with_lock_helper() {
            let m = Mutex::new(0i32);
            with_lock(&m, |v| *v += 1);
            with_lock(&m, |v| *v += 1);
            assert_eq!(*m.lock().unwrap(), 2);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_shared_counter() {
            assert_eq!(shared_counter(), 1000);
        }
    
        #[test]
        fn test_bank_account() {
            let (balance, txns) = bank_account_demo();
            assert_eq!(balance, 300); // 5*100 - 200
            assert_eq!(txns, 6); // 5 deposits + 1 withdrawal
        }
    
        #[test]
        fn test_collect_to_vec() {
            let v = collect_to_vec();
            assert_eq!(v, vec![0, 1, 2, 3, 4]);
        }
    
        #[test]
        fn test_mutex_protects_from_data_race() {
            // If counter were unprotected, this would be UB/wrong
            assert_eq!(shared_counter(), 1000);
        }
    
        #[test]
        fn test_with_lock_helper() {
            let m = Mutex::new(0i32);
            with_lock(&m, |v| *v += 1);
            with_lock(&m, |v| *v += 1);
            assert_eq!(*m.lock().unwrap(), 2);
        }
    }

    Deep Comparison

    Mutex-Protected State — Comparison

    Core Insight

    Both languages use mutexes for shared mutable state, but Rust's type system enforces correct use: Mutex<T> wraps the data itself, so you physically cannot access it without locking. OCaml's Mutex.t is a separate object — the programmer must remember to lock/unlock around every access.

    OCaml Approach

  • Mutex.create () creates a mutex independent of the data
  • Mutex.lock m / Mutex.unlock m must be called manually — easy to forget
  • • Exception-unsafe: if f() throws, you must catch and unlock (bracket pattern)
  • with_lock helper function needed for exception safety
  • • Data lives in ref cells separate from the mutex
  • Rust Approach

  • Mutex::new(data) wraps data and mutex together — inseparable
  • mutex.lock().unwrap() returns a MutexGuard<T> — acts like &mut T
  • • Guard unlocks automatically when dropped (RAII) — exception safe
  • Arc<Mutex<T>> for sharing across threads (atomically ref-counted)
  • • Compiler prevents accessing data without locking — zero runtime overhead
  • Comparison Table

    ConceptOCamlRust
    CreateMutex.create () + ref dataMutex::new(data)
    LockMutex.lock mm.lock().unwrap()
    UnlockMutex.unlock m (manual)Drop the MutexGuard (RAII)
    Access dataAccess ref directlyDereference guard: *guard
    Share across threadsref in closureArc::clone(&mutex)
    Exception safetyManual bracket patternAutomatic via RAII
    Forget to unlockPossible (deadlock)Impossible — type system prevents

    std vs tokio

    Aspectstd versiontokio version
    RuntimeOS threads via std::threadAsync tasks on tokio runtime
    Synchronizationstd::sync::Mutex, Condvartokio::sync::Mutex, channels
    Channelsstd::sync::mpsc (unbounded)tokio::sync::mpsc (bounded, async)
    BlockingThread blocks on lock/recvTask yields, runtime switches tasks
    OverheadOne OS thread per taskMany tasks per thread (M:N)
    Best forCPU-bound, simple concurrencyI/O-bound, high-concurrency servers

    Exercises

  • Verify that 10 threads × 100 increments = 1000 (no data races).
  • Implement withdraw on BankAccount that returns Err if balance would go negative, while holding the lock for the entire check-and-debit operation.
  • Implement a deadlock scenario and explain why it deadlocks: two threads each try to acquire two mutexes in opposite orders.
  • Replace Arc<Mutex<Vec<T>>> with Arc<RwLock<Vec<T>>> for a read-heavy workload and benchmark.
  • Implement try_lock that returns None if the lock is contended rather than blocking.
  • Open Source Repos