Mutex and RwLock
Shared Mutable State: Use With Discipline
Rust allows shared mutable state through synchronization primitives such as Mutex and RwLock. These tools are powerful but introduce contention and potential bottlenecks. In production systems, they must be used deliberately.
Production mindset:
- Prefer immutable sharing and message passing first
- Use locks only when shared mutation is truly required
- Keep critical sections small
Mutex: Exclusive Access
Mutex<T> provides mutual exclusion. Only one thread can access the inner value at a time.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut guard = c.lock().unwrap();
*guard += 1;
}));
}
for h in handles {
h.join().unwrap();
}
println!("counter={}", *counter.lock().unwrap());
}
The lock() call blocks until the mutex is available. The returned guard releases the lock when dropped.
Lock Scope Matters
Always minimize the time you hold a lock.
Bad pattern (long critical section):
let mut guard = data.lock().unwrap(); do_expensive_work(&mut *guard);
Better pattern:
let value = {
let mut guard = data.lock().unwrap();
*guard += 1;
*guard
};
do_expensive_work(value);
Production rule: never hold a lock across network calls, disk I/O, or long CPU work.
RwLock: Many Readers, One Writer
RwLock<T> allows multiple readers or one writer. It is useful when reads are frequent and writes are rare.
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let reader = {
let d = Arc::clone(&data);
thread::spawn(move || {
let guard = d.read().unwrap();
println!("read: {:?}", *guard);
})
};
let writer = {
let d = Arc::clone(&data);
thread::spawn(move || {
let mut guard = d.write().unwrap();
guard.push(4);
})
};
reader.join().unwrap();
writer.join().unwrap();
}
Production note: RwLock is not always faster than Mutex. Under heavy write load, it may perform worse due to additional coordination overhead.
Deadlocks: The Hidden Risk
Deadlocks happen when two threads wait on each other’s locks. Rust does not prevent logical deadlocks.
Common cause:
- Lock A then lock B in one thread
- Lock B then lock A in another thread
Production mitigation:
- Define a consistent lock ordering policy
- Prefer smaller, independent locks
- Avoid nested locking where possible
Poisoning Behavior
If a thread panics while holding a lock, the mutex becomes poisoned. Subsequent lock attempts return an error.
let guard = mutex.lock().unwrap();
Production note: decide whether to unwrap (crash fast) or handle poisoned locks explicitly.
Arc + Mutex Pattern
In multi-threaded Rust, shared state commonly appears as Arc<Mutex<T>>. This is correct but easy to overuse.
Production rule: if you find yourself wrapping everything in Arc<Mutex<T>>, reconsider the design. Message passing may be cleaner.
Contention and Performance
Under load, locks become contention points. Symptoms:
- High CPU but low throughput
- Long request latency spikes
- Thread blocking visible in profiling
Mitigations:
- Reduce lock granularity
- Use sharded data structures
- Replace shared mutation with channels
- Use atomic counters for simple numeric updates
When to Prefer Channels Over Locks
If you are coordinating work between threads, channels are often simpler and safer than shared state.
Prefer channels when:
- You are modeling a job queue
- You can centralize state updates in a single worker
- You want clear ownership flow
Production Checklist
- Use Mutex for simple exclusive access
- Use RwLock only when read-heavy and write-light
- Keep lock scope minimal
- Never hold locks during I/O or long work
- Define consistent lock ordering
- Consider channels before adding shared mutable state
Mutex and RwLock are essential tools, but they introduce coordination cost. In production Rust systems, they should be used carefully, deliberately, and with observability in mind.