Rust 63 🦀 Shared State Concurrency with Mutex (Mutex) and RwLock
Shared mutable state is the classic source of concurrency bugs. Two threads incrementing the same counter without coordination can produce a wrong total because the increment is three operations—read, add, write—and the threads can interleave arbitrarily . Rust does not eliminate shared state, but it makes the dangerous parts impossible to ignore. The Mutex<T> type wraps the data itself, so you cannot touch the value without acquiring the lock. The RwLock<T> type relaxes the mutual exclusion for read-heavy workloads, allowing many concurrent readers while keeping writers exclusive.
The design philosophy here is the same as everywhere else in Rust: move the invariant into the type system. A Mutex<i32> is not an i32. You cannot read or write it without calling lock(), and you cannot forget to unlock because the MutexGuard released on drop . This eliminates the two classic mutex bugs—forgetting to lock and forgetting to unlock—by making the first a type error and the second an automatic consequence of scope. The same ownership discipline that makes channels safe makes shared memory safe when the right primitive is used.
This chapter covers Mutex<T>, RwLock<T>, and the Arc wrapper that makes them shareable across threads. You will learn the MutexGuard pattern, the poisoning mechanism that marks data as inconsistent after a panic, the difference between read and write locks, and the deadlock shapes that the compiler cannot catch. We will also cover when to use Arc<Mutex<T>> versus Arc<RwLock<T>> versus avoiding shared locks entirely.
Key point: Mutex<T> provides exclusive access through a MutexGuard that dereferences to the inner data and releases the lock when dropped; RwLock<T> allows many concurrent readers or one exclusive writer; both are usually wrapped in Arc for shared ownership across threads.
Why shared state works this way in Rust
The data lives inside the lock. In most languages, the lock and the data are separate: you acquire a lock object, then touch the data, hoping you remembered the first step. In Rust, the data is structurally inside the Mutex. The Mutex::new(value) constructor consumes the value, and the only way to reach it again is through lock() . There is no gap where you can accidentally access unprotected data because the type does not expose any other path.
The guard is the key. lock() returns a MutexGuard<T>, a smart pointer that implements Deref to T and DerefMut to &mut T . The guard also implements Drop, which releases the lock. This means the lock is held for exactly the lexical scope of the guard, no more and no less. If the thread panics while holding the guard, unwinding drops it, and the lock is released .
Poisoning marks inconsistency. If a thread panics while holding the lock, the mutex becomes poisoned. Subsequent lock() calls return Err(PoisonError) instead of a guard . This is not a lockout—the guard is still recoverable via into_inner()—but it signals that the protected data may be in an inconsistent state. Poisoning is Rust’s way of propagating “something went wrong here” to threads that did not directly observe the panic.
RwLock splits the access mode. A Mutex does not distinguish between readers and writers. An RwLock does: any number of readers can hold the lock simultaneously, but a writer requires exclusive access . This is beneficial when reads vastly outnumber writes and the read operation is non-trivial. For a simple counter increment, RwLock is usually not worth the overhead .
Arc provides shared ownership. A bare Mutex<T> cannot be shared across threads because it is not Clone and moving it into one thread removes it from the others. Arc<T> is an atomically reference-counted pointer that can be cloned cheaply and shared . Arc<Mutex<T>> is the canonical pattern: Arc for shared ownership, Mutex for synchronized mutation.
The compiler cannot catch deadlocks. Deadlocks are a logic error, not a type error. The classic shape is two threads acquiring two locks in opposite orders . Rust’s borrow checker does not see this. The universal fix is a consistent global lock order; the structural fix is to avoid holding multiple locks simultaneously or to pass ownership through channels instead .
a. Creating a Mutex and using the guard
The Mutex::new constructor takes the value to protect and returns a Mutex<T>. The lock method blocks until the lock is available and returns a LockResult<MutexGuard<T>> .
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
} // guard drops here, lock released
println!("{:?}", m);
}
The lock().unwrap() pattern is standard: lock() returns Result because of poisoning, and unwrap() panics if the mutex is poisoned. The guard num behaves like a mutable reference to the inner i32. When the inner scope ends, the guard drops, releasing the lock .
The try_lock method does not block. It returns Ok(guard) if the lock is available, or Err(TryLockError) immediately if it is not . This is useful for non-blocking checks, but it introduces the risk of “busy-waiting” if used in a loop without a backoff strategy.
b. Sharing a Mutex across threads with Arc
A Mutex alone cannot be moved into multiple threads. The Arc wrapper provides shared ownership, and each thread gets a clone of the Arc that points to the same Mutex .
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0u64));
let handles: Vec<_> = (0..10)
.map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
for _ in 0..100 {
let mut num = c.lock().unwrap();
*num += 1;
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
println!("{}", *counter.lock().unwrap());
}
The pattern is Arc::new(Mutex::new(value)), Arc::clone per thread, move the clone into the closure, lock-mutate-drop . The counter is guaranteed to reach 1000 because each increment is serialized by the mutex. Without the mutex, the increments would interleave and the final value would be unpredictable.
The Arc<Mutex<T>> combination is structurally the same as Rc<RefCell<T>> from single-threaded Rust. Arc provides shared ownership like Rc, and Mutex provides interior mutability like RefCell . The difference is that Arc and Mutex are thread-safe, at the cost of atomic operations and lock overhead.
c. Read-heavy workloads with RwLock
An RwLock<T> distinguishes between read access and write access. Multiple readers can hold the lock simultaneously, but a writer requires exclusive access .
use std::sync::RwLock;
fn main() {
let lock = RwLock::new(5);
// Multiple readers
{
let r1 = lock.read().unwrap();
let r2 = lock.read().unwrap();
println!("{} {}", *r1, *r2);
} // both read guards drop here
// One writer
{
let mut w = lock.write().unwrap();
*w += 1;
} // write guard drops here
}
The read method returns a RwLockReadGuard, and write returns a RwLockWriteGuard. Both implement Deref (and the write guard also DerefMut) and both release the lock on drop. The read guard is Deref<Target = T>, giving immutable access; the write guard is DerefMut<Target = T>, giving mutable access .
A crucial asymmetry: RwLock is poisoned only if a panic occurs while a write lock is held. If a panic occurs in a reader, the lock is not poisoned . This reflects the fact that a reader cannot leave the data in an inconsistent state.
RwLock is beneficial only when reads are significantly more frequent than writes and the read operation is non-trivial. For a simple field read, the overhead of the read lock is comparable to a mutex, and the complexity is not justified . Use RwLock when the read path does meaningful work—searching a large map, scanning a collection—and writes are rare.
Complete Example Session
The following session builds a shared counter with Arc<Mutex<T>>, a read-heavy shared configuration with Arc<RwLock<T>>, and demonstrates poisoning recovery.
// ============================================
// PART 1: IMPORTS
// ============================================
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
// ============================================
// PART 2: BASIC MUTEX OPERATION
// ============================================
fn basic_mutex() -> i32 {
let m = Mutex::new(5);
{
let mut guard = m.lock().unwrap();
*guard = 6;
}
*m.lock().unwrap()
}
// ============================================
// PART 3: SHARED COUNTER WITH ARC<MUTEX<T>>
// ============================================
fn shared_counter() -> u64 {
let counter = Arc::new(Mutex::new(0u64));
let handles: Vec<_> = (0..10)
.map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
for _ in 0..100 {
*c.lock().unwrap() += 1;
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
*counter.lock().unwrap()
}
// ============================================
// PART 4: RWLock FOR READ-HEAVY ACCESS
// ============================================
fn read_heavy() -> Vec<i32> {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
// Spawn readers
let readers: Vec<_> = (0..5)
.map(|_| {
let d = Arc::clone(&data);
thread::spawn(move || {
let guard = d.read().unwrap();
guard.iter().sum::<i32>()
})
})
.collect();
// One writer
{
let mut w = data.write().unwrap();
w.push(4);
}
readers.into_iter().map(|h| h.join().unwrap()).collect()
}
// ============================================
// PART 5: POISON RECOVERY
// ============================================
fn poison_recovery() -> i32 {
let m = Arc::new(Mutex::new(42));
// Poison the mutex by panicking while holding it
let m2 = Arc::clone(&m);
let _ = thread::spawn(move || {
let _guard = m2.lock().unwrap();
panic!("intentional panic");
})
.join();
// Recover the data from the poisoned mutex
match m.lock() {
Ok(guard) => *guard,
Err(poisoned) => {
let guard = poisoned.into_inner();
*guard
}
}
}
// ============================================
// PART 6: TRY_LOCK FOR NON-BLOCKING ACCESS
// ============================================
fn try_lock_demo() -> Option<i32> {
let m = Mutex::new(10);
{
let _guard = m.lock().unwrap();
// While holding the lock, try_lock fails
if m.try_lock().is_err() {
return Some(-1);
}
}
// After releasing, try_lock succeeds
m.try_lock().map(|g| *g).ok()
}
// ============================================
// PART 7: MULTIPLE READERS SIMULTANEOUSLY
// ============================================
fn multiple_readers() -> bool {
let lock = RwLock::new(5);
let r1 = lock.read().unwrap();
let r2 = lock.read().unwrap();
// Both readers hold the lock simultaneously
*r1 == 5 && *r2 == 5
}
// ============================================
// PART 8: WRITER EXCLUDES READERS
// ============================================
fn writer_excludes() -> bool {
let lock = RwLock::new(5);
let w = lock.write().unwrap();
// try_read should fail while writer holds lock
let failed = lock.try_read().is_err();
drop(w);
failed
}
// ============================================
// PART 9: ATOMIC VS MUTEX FOR COUNTERS
// ============================================
use std::sync::atomic::{AtomicU64, Ordering};
fn atomic_counter() -> u64 {
let counter = Arc::new(AtomicU64::new(0));
let handles: Vec<_> = (0..10)
.map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
for _ in 0..100 {
c.fetch_add(1, Ordering::Relaxed);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
counter.load(Ordering::Relaxed)
}
// ============================================
// PART 10: MAIN
// ============================================
fn main() {
println!("basic: {}", basic_mutex());
println!("shared: {}", shared_counter());
println!("read-heavy: {:?}", read_heavy());
println!("poison: {}", poison_recovery());
println!("try-lock: {:?}", try_lock_demo());
println!("readers: {}", multiple_readers());
println!("exclusive: {}", writer_excludes());
println!("atomic: {}", atomic_counter());
}
The ten parts cover basic mutex use, the shared counter pattern with Arc, RwLock for read-heavy access, poison recovery, try_lock, simultaneous readers, writer exclusion, and the atomic alternative for simple counters. What the session demonstrates is that shared state in Rust is not dangerous when the right primitive is used, and that the type system enforces the discipline that other languages rely on developer memory.
Quick Reference
Mutex and RwLock API
| Item | Signature | Meaning |
|---|---|---|
Mutex::new | T -> Mutex<T> | Wrap a value |
Mutex::lock | &self -> LockResult<MutexGuard<T>> | Blocking exclusive access |
Mutex::try_lock | &self -> TryLockResult<MutexGuard<T>> | Non-blocking attempt |
RwLock::new | T -> RwLock<T> | Wrap a value |
RwLock::read | &self -> LockResult<RwLockReadGuard<T>> | Shared access |
RwLock::write | &self -> LockResult<RwLockWriteGuard<T>> | Exclusive access |
RwLock::try_read | &self -> TryLockResult<RwLockReadGuard<T>> | Non-blocking read |
RwLock::try_write | &self -> TryLockResult<RwLockWriteGuard<T>> | Non-blocking write |
Guard Types
| Guard | Deref | DerefMut | Releases On Drop |
|---|---|---|---|
MutexGuard<T> | &T | &mut T | Yes |
RwLockReadGuard<T> | &T | No | Yes |
RwLockWriteGuard<T> | &T | &mut T | Yes |
Poisoning Behavior
| Situation | Mutex | RwLock |
|---|---|---|
| Panic while holding | Poisoned | Poisoned (write only) |
| Panic in reader | N/A | Not poisoned |
lock() after poison | Err(PoisonError) | Err(PoisonError) |
| Recovery | into_inner() | into_inner() |
When to Use What
| Situation | Primitive |
|---|---|
| Simple counter, high contention | Arc<AtomicU64> |
| Compound state, mixed read/write | Arc<Mutex<T>> |
| Read-heavy, non-trivial reads | Arc<RwLock<T>> |
| Producer-consumer, owned-at-a-time | Channel, no lock |
Best Practices
✅ Do This:
let m = Mutex::new(5); // ✅ data inside lock
let guard = m.lock().unwrap(); *guard = 6; // ✅ mutate via guard
let counter = Arc::new(Mutex::new(0)); // ✅ Arc for sharing
let c = Arc::clone(&counter); // ✅ clone Arc, not Mutex
let mut w = lock.write().unwrap(); *w += 1; // ✅ write for mutation
poisoned.into_inner() // ✅ recover poisoned data
c.try_lock().ok() // ✅ non-blocking when busy
❌ Don’t Do This:
static mut COUNTER: i32 = 0; // ❌ static mut is unsafe
let x = m.lock().unwrap(); let y = m.lock().unwrap(); // ❌ deadlock, same thread
let guard = m.lock(); // no unwrap, forgotten Result // ❌ poisoning ignored
let counter = Mutex::new(0); thread::spawn(move || ...); // ❌ moved, not shared
lock.read().unwrap(); lock.write().unwrap(); // ❌ write while reading
let _ = m.lock(); // guard dropped immediately // ❌ lock released too early
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Deadlock on same thread | Calling lock() twice without dropping | Use one guard, scope it tightly |
| Deadlock across threads | Two locks acquired in opposite orders | Consistent global lock order |
| Poisoned mutex crashes | .unwrap() on lock() after panic | Use into_inner() to recover |
Lock held across .await | Guard lives across async boundary | Scope guard, or use tokio::sync::Mutex |
Arc forgotten | Bare Mutex moved into one thread | Arc::new(Mutex::new(value)) |
RwLock slower than Mutex | Read lock overhead for trivial reads | Use Mutex unless reads are heavy |
| Guard dropped too early | Temporary guard not bound to variable | let guard = m.lock().unwrap() |
| Writer starvation | Readers keep acquiring read locks | Use a fair implementation or Mutex |
Real-World Examples
1. Shared counter
let counter = Arc::new(Mutex::new(0));
*counter.lock().unwrap() += 1;
2. Shared configuration map
let config: Arc<RwLock<HashMap<String, String>>> = Arc::new(RwLock::new(HashMap::new()));
config.write().unwrap().insert("key".into(), "value".into());
3. Job result collection
let results = Arc::new(Mutex::new(Vec::new()));
results.lock().unwrap().push(output);
4. Poison recovery
let guard = mutex.lock().unwrap_or_else(|e| e.into_inner());
5. Non-blocking lock attempt
if let Ok(mut guard) = mutex.try_lock() { *guard += 1; }
6. Read-heavy lookup
let guard = cache.read().unwrap();
guard.get(&key)
7. Atomic for simple counters
counter.fetch_add(1, Ordering::Relaxed);
8. Scope guard tightly
{
let mut guard = mutex.lock().unwrap();
*guard += 1;
} // released here
9. Clone Arc per thread
let c = Arc::clone(&counter);
thread::spawn(move || { *c.lock().unwrap() += 1; });
10. Detect poisoning
match mutex.lock() { Ok(g) => use(g), Err(p) => recover(p.into_inner()) }
Visual
Mutex and Arc Layers
┌──────────────────────────────────────────────┐
│ Arc<Mutex<T>> │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ Arc (shared ownership) │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ Mutex<T> (exclusive access) │ │ │
│ │ │ ┌────────────────────────────┐ │ │ │
│ │ │ │ T (the actual data) │ │ │ │
│ │ │ └────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
│ │
│ Arc::clone → multiple owners │
│ lock() → MutexGuard → Deref/DerefMut → T │
│ guard drop → unlock │
└──────────────────────────────────────────────┘
MutexGuard Lifecycle
┌──────────────────────────────────────────────┐
│ let guard = mutex.lock().unwrap(); │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ MutexGuard │ │
│ │ - Deref to &T │ │
│ │ - DerefMut to &mut T │ │
│ │ - Drop releases lock │ │
│ └────────────────┬───────────────────────┘ │
│ │ │
│ *guard = new_value; │
│ │ │
│ guard goes out of scope │
│ │ │
│ ▼ │
│ lock released automatically │
└──────────────────────────────────────────────┘
RwLock Access Modes
┌──────────────────────────────────────────────┐
│ RwLock<T> │
│ │
│ READERS (shared) │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ r1 │ │ r2 │ │ r3 │ all simultaneous│
│ └──────┘ └──────┘ └──────┘ │
│ │
│ WRITER (exclusive) │
│ ┌──────────────────────────┐ │
│ │ w │ blocks all │
│ └──────────────────────────┘ │
│ │
│ Readers and writer are mutually exclusive. │
└──────────────────────────────────────────────┘
Poisoning State Machine
┌──────────────────────────────────────────────┐
│ LOCKED (normal) │
│ thread holds guard │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ drop panic │
│ │ │ │
│ ▼ ▼ │
│ UNLOCKED POISONED │
│ (normal) lock() returns Err │
│ into_inner() recovers data │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Exclusive lock | Mutex<T> |
| Shared/exclusive lock | RwLock<T> |
| Shared ownership | Arc<T> |
| Canonical pattern | Arc<Mutex<T>> |
| Lock method | lock(), read(), write() |
| Guard types | MutexGuard, RwLockReadGuard, RwLockWriteGuard |
| Guard drop | Releases lock automatically |
| Poisoning | Panic while holding poisons the lock |
| Recovery | PoisonError::into_inner() |
| Non-blocking | try_lock(), try_read(), try_write() |
| Simple counter | Arc<AtomicU64> |
Key takeaways:
- The data lives inside the lock.
Mutex<T>andRwLock<T>own the protected value, so you cannot access it without acquiring the lock . - The guard is the key.
MutexGuardimplementsDeref/DerefMutand releases the lock on drop, making forgot-to-unlock impossible . Arcprovides shared ownership.Arc<Mutex<T>>is the canonical pattern for shared mutable state across threads .- Poisoning signals inconsistency. A panic while holding the lock marks it poisoned; recovery via
into_inner()is available but should be deliberate . RwLockallows concurrent readers. Use it when reads vastly outnumber writes and the read path is non-trivial .RwLockis poisoned only by writer panics. A reader panic does not poison the lock .- Deadlocks are design errors. The compiler cannot catch two locks acquired in opposite orders; use consistent ordering .
- Simple counters should use atomics.
Arc<AtomicU64>avoids lock overhead for single-value increments .
Remember: Shared state in Rust is safe because the dangerous parts are encoded in the type system. A Mutex<T> wraps the data, forcing you through lock() and returning a guard that unlocks on drop. RwLock<T> relaxes this for read-heavy workloads, allowing many readers while keeping writers exclusive. Wrap either in Arc<T> when multiple threads need ownership. The compiler cannot catch deadlocks or prevent holding a guard too long, so discipline still matters: scope guards tightly, order locks consistently, and reach for atomics or channels when shared locks are not the right tool.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!