advanced90 minutesLesson 3 of 10

Shared State — Mutex, RwLock, Arc, and Atomics

Master shared-state concurrency with Mutex, RwLock, Arc, and atomic types for lock-free programming

Shared State — Mutex, RwLock, Arc, and Atomics

While message passing is great, shared state is sometimes the right tool. Rust's type system ensures shared state is accessed safely.

Mutex<T> — Mutual Exclusion

rust
use std::sync::Mutex; fn main() { let mutex = Mutex::new(42); // Lock to access data { let mut guard = mutex.lock().unwrap(); *guard += 1; println!("Inside lock: {guard}"); } // Lock released here println!("{:?}", mutex); // Mutex { data: 43, ... } }

Mutex Internals

Mutex<T> { data: T, // Protected data inner: { // OS-level mutex locked: AtomicBool, queue: WaitQueue, }, }

Poisoning

When a thread panics while holding a lock, the mutex is poisoned:

rust
use std::sync::Mutex; fn main() { let mutex = Mutex::new(10); // Thread panics while holding lock let handle = std::thread::spawn(move || { let guard = mutex.lock().unwrap(); panic!("panic while holding lock"); drop(guard); // Never reached }); assert!(handle.join().is_err()); // mutex is now poisoned }
MethodBehavior on Poison
lock().unwrap()Panics if poisoned
lock().unwrap_or_else(poisoned)Can recover with .into_inner()
.into_inner()Consumes mutex, returns data
⚠️Warning

A poisoned mutex means the protected data may be in an inconsistent state. Use lock().unwrap_or_else(|e| e.into_inner()) to recover if you're sure it's safe.

MutexGuard — RAII Lock

rust
use std::sync::Mutex; fn main() { let mutex = Mutex::new(vec![1, 2, 3]); // MutexGuard implements Deref and DerefMut let mut guard = mutex.lock().unwrap(); guard.push(4); // With map — access a field inside the lock let first: std::sync::MutexGuard<'_, i32> = std::sync::MutexGuard::map(guard, |v| &mut v[0]); *first = 10; // guard is no longer valid, first holds lock drop(first); // Lock released }

RwLock<T> — Read-Write Lock

Multiple readers OR one writer:

rust
use std::sync::RwLock; fn main() { let rwlock = RwLock::new(42); // Multiple concurrent readers { let r1 = rwlock.read().unwrap(); let r2 = rwlock.read().unwrap(); // OK: multiple readers println!("{r1} {r2}"); } // Readers released // Exclusive writer { let mut w = rwlock.write().unwrap(); *w += 1; } }
LockReadersWritersUse Case
Mutex<T>1 at a time1 at a timeSimple exclusive access
RwLock<T>Unlimited1 at a timeRead-heavy workloads
Success

Use RwLock when reads vastly outnumber writes. For workloads with frequent writes, Mutex is usually faster due to lower overhead.

RwLock Starvation

rust
use std::sync::RwLock; use std::thread; fn main() { let lock = RwLock::new(0); let lock = std::sync::Arc::new(lock); // Writer may starve if readers are continuous // Use RwLock::try_read() and RwLock::try_write() for non-blocking // Use parking_lot::RwLock for fair locking }
MethodBlocking?Returns
read()YesRwLockReadGuard<T>
try_read()NoResult<RwLockReadGuard<T>, TryLockError>
write()YesRwLockWriteGuard<T>
try_write()NoResult<RwLockWriteGuard<T>, TryLockError>

Arc — Atomic Reference Counting

Arc<T> enables shared ownership across threads:

rust
use std::sync::Arc; use std::thread; fn main() { let data = Arc::new(vec![1, 2, 3]); let mut handles = vec![]; for _ in 0..5 { let data = Arc::clone(&data); // Atomic increment handles.push(thread::spawn(move || { println!("{:?}", data); })); } for handle in handles { handle.join().unwrap(); } // All clones dropped, data freed }

Arc Internals

Arc<T> { ptr: NonNull<ArcInner<T>>, } ArcInner<T> { strong: AtomicUsize, // Reference count weak: AtomicUsize, // Weak reference count data: T, }

Weak — Breaking Cycles

rust
use std::sync::{Arc, Weak}; fn main() { let shared: Arc<String> = Arc::new("hello".to_string()); let weak: Weak<String> = Arc::downgrade(&shared); // Upgrade to check if still alive match weak.upgrade() { Some(data) => println!("Still alive: {data}"), None => println!("Data was dropped"), } drop(shared); assert!(weak.upgrade().is_none()); }

Atomics — Lock-Free Programming

Atomic types provide lock-free, thread-safe operations:

rust
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::thread; fn main() { let flag = AtomicBool::new(false); let counter = AtomicUsize::new(0); let flag = std::sync::Arc::new(flag); let counter = std::sync::Arc::new(counter); let mut handles = vec![]; for _ in 0..10 { let flag = Arc::clone(&flag); let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { counter.fetch_add(1, Ordering::SeqCst); flag.store(true, Ordering::SeqCst); })); } for handle in handles { handle.join().unwrap(); } println!("counter: {}", counter.load(Ordering::SeqCst)); println!("flag: {}", flag.load(Ordering::SeqCst)); }

Atomic Types

TypeSizeUse Case
AtomicBool1 byteFlags, control signals
AtomicI32 / AtomicU324 bytesCounters, state
AtomicI64 / AtomicU648 bytesLarger counters
AtomicUsizearch bytesPointer-sized counters
AtomicPtr<T>arch bytesLock-free data structures

Ordering

Memory ordering controls visibility of atomic operations:

rust
use std::sync::atomic::Ordering; // Relaxed — no ordering guarantees (fastest) x.store(5, Ordering::Relaxed); y.load(Ordering::Relaxed); // Acquire — subsequent reads see the write // Release — previous writes are visible to acquire x.store(5, Ordering::Release); y.load(Ordering::Acquire); // Sees x = 5 // SeqCst — strongest guarantee // All threads see operations in the same order x.store(5, Ordering::SeqCst); y.load(Ordering::SeqCst);
⚠️Warning
| Ordering | Guarantee | Cost | |----------|-----------|------| | `Relaxed` | No ordering | Zero | | `Acquire`/`Release` | happens-before | Moderate | | `AcqRel` | Acquire + Release | Moderate | | `SeqCst` | Sequential consistency | Highest (but usually fine) |

Atomic Operations

rust
use std::sync::atomic::{AtomicUsize, Ordering}; fn main() { let counter = AtomicUsize::new(0); // Fetch-and-modify counter.fetch_add(5, Ordering::SeqCst); counter.fetch_sub(3, Ordering::SeqCst); // Compare-and-swap (CAS) let mut current = counter.load(Ordering::SeqCst); loop { let new = current + 1; match counter.compare_exchange( current, new, Ordering::SeqCst, Ordering::SeqCst, ) { Ok(_) => break, Err(actual) => current = actual, // Retry with updated value } } // Swap let old = counter.swap(100, Ordering::SeqCst); println!("Final: {}", counter.load(Ordering::SeqCst)); // 100 }

Real-World: Concurrent Counter

rust
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::thread; struct ConcurrentCounter { value: AtomicUsize, } impl ConcurrentCounter { fn new() -> Self { ConcurrentCounter { value: AtomicUsize::new(0) } } fn increment(&self) -> usize { self.value.fetch_add(1, Ordering::SeqCst) } fn get(&self) -> usize { self.value.load(Ordering::SeqCst) } } fn main() { let counter = Arc::new(ConcurrentCounter::new()); let mut handles = vec![]; for _ in 0..100 { let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { for _ in 0..1000 { counter.increment(); } })); } for handle in handles { handle.join().unwrap(); } println!("Final count: {}", counter.get()); // 100000 }

Practice Questions

  1. What's the difference between Mutex and RwLock?
  2. What does it mean when a mutex is poisoned?
  3. How do you recover from a poisoned mutex?
  4. What's the difference between Arc and Rc?
  5. When should you use Weak instead of Arc?
  6. What are atomic types and when are they useful?
  7. What do the different memory orderings mean?
  8. How does compare-and-swap (CAS) work?
  9. What's the advantage of RwLock over Mutex for read-heavy workloads?
  10. How do you atomically increment a shared counter?
Progress30%