986 Mutex Basics
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
Arc<Mutex<T>> for shared mutable state: Arc::clone shares the pointer, Mutex::lock protects accessMutex::lock returns MutexGuard<T> — a RAII guard that releases the lock when dropped*n += 1 to increment through the guard's DerefMut implementationMutex.lock / Mutex.unlock or Mutex.protectCode 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
| Aspect | Rust | OCaml |
|---|---|---|
| Lock scope | RAII guard — automatic release | Manual lock/unlock or Mutex.protect |
| Shared ownership | Arc<Mutex<T>> | Mutex.t + ref (shared implicitly) |
| Poisoning | Mutex poisons on thread panic | No equivalent — mutex stays usable |
| Data ownership | Mutex owns T | Mutex 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);
}
}#[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 dataMutex.lock m / Mutex.unlock m must be called manually — easy to forgetf() throws, you must catch and unlock (bracket pattern)with_lock helper function needed for exception safetyref cells separate from the mutexRust Approach
Mutex::new(data) wraps data and mutex together — inseparablemutex.lock().unwrap() returns a MutexGuard<T> — acts like &mut TArc<Mutex<T>> for sharing across threads (atomically ref-counted)Comparison Table
| Concept | OCaml | Rust |
|---|---|---|
| Create | Mutex.create () + ref data | Mutex::new(data) |
| Lock | Mutex.lock m | m.lock().unwrap() |
| Unlock | Mutex.unlock m (manual) | Drop the MutexGuard (RAII) |
| Access data | Access ref directly | Dereference guard: *guard |
| Share across threads | ref in closure | Arc::clone(&mutex) |
| Exception safety | Manual bracket pattern | Automatic via RAII |
| Forget to unlock | Possible (deadlock) | Impossible — type system prevents |
std vs tokio
| Aspect | std version | tokio version |
|---|---|---|
| Runtime | OS threads via std::thread | Async tasks on tokio runtime |
| Synchronization | std::sync::Mutex, Condvar | tokio::sync::Mutex, channels |
| Channels | std::sync::mpsc (unbounded) | tokio::sync::mpsc (bounded, async) |
| Blocking | Thread blocks on lock/recv | Task yields, runtime switches tasks |
| Overhead | One OS thread per task | Many tasks per thread (M:N) |
| Best for | CPU-bound, simple concurrency | I/O-bound, high-concurrency servers |
Exercises
withdraw on BankAccount that returns Err if balance would go negative, while holding the lock for the entire check-and-debit operation.Arc<Mutex<Vec<T>>> with Arc<RwLock<Vec<T>>> for a read-heavy workload and benchmark.try_lock that returns None if the lock is contended rather than blocking.