RwLock<T>
std::sync::RwLock<T> (Reader-Writer Lock) is a thread-safe interior mutability primitive that mirrors Rust's borrowing rules across threads: it allows either any number of concurrent readers (RwLockReadGuard<'_, T>, dereferencing to &T) or at most one exclusive writer (RwLockWriteGuard<'_, T>, dereferencing to &mut T) at any moment.
Reader-Writer Concurrency Model
Unlike Mutex<T>, which serializes even read-only accesses one thread at a time, RwLock<T> lets multiple threads read the inner value simultaneously without blocking each other, blocking only when a writer requests exclusive access.
Mode 1 — Concurrent Readers (N threads hold RwLockReadGuard<'_, T>):
Thread 1 (.read()) ----\
Thread 2 (.read()) ----+---> [ RwLock<T>: shared &T access ] (Write requests wait)
Thread 3 (.read()) ----/
Mode 2 — Exclusive Writer (1 thread holds RwLockWriteGuard<'_, T>):
Thread 4 (.write()) -------> [ RwLock<T>: exclusive &mut T ] (All read & write requests wait)
Core RwLock<T> Methods
| Method | Return Type | Behavior |
|---|---|---|
RwLock::new(val) |
RwLock<T> |
Creates a new unlocked RwLock<T> (const fn) |
.read() |
LockResult<RwLockReadGuard<'_, T>> |
Blocks until no writer holds the lock; grants shared &T access |
.try_read() |
TryLockResult<RwLockReadGuard<'_, T>> |
Non-blocking attempt to acquire a shared read guard |
.write() |
LockResult<RwLockWriteGuard<'_, T>> |
Blocks until all readers and writers release the lock; grants &mut T access |
.try_write() |
TryLockResult<RwLockWriteGuard<'_, T>> |
Non-blocking attempt to acquire an exclusive write guard |
.get_mut() |
LockResult<&mut T> |
Zero-cost mutable access when holding &mut RwLock<T> |
.into_inner() |
LockResult<T> |
Consumes the lock and returns the inner T |
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let routing_table = Arc::new(RwLock::new(HashMap::from([
(String::from("/health"), String::from("200 OK")),
(String::from("/metrics"), String::from("prometheus")),
])));
thread::scope(|s| {
// Spawn one writer thread to register a new route:
let writer_table = Arc::clone(&routing_table);
s.spawn(move || {
let mut write_guard = writer_table.write().unwrap();
write_guard.insert(String::from("/v1/users"), String::from("user-service"));
});
// Spawn multiple concurrent reader threads:
for id in 0..3 {
let reader_table = Arc::clone(&routing_table);
s.spawn(move || {
let read_guard = reader_table.read().unwrap();
println!(
"reader {} sees /health -> {:?}",
id,
read_guard.get("/health")
);
});
}
});
println!(
"total routes registered: {}",
routing_table.read().unwrap().len()
);
}
reader 0 sees /health -> Some("200 OK")
reader 1 sees /health -> Some("200 OK")
reader 2 sees /health -> Some("200 OK")
total routes registered: 3
Trait Bounds Required for RwLock<T> vs. Mutex<T>
Because multiple threads can simultaneously hold RwLockReadGuard<'_, T> and access &T concurrently, RwLock<T> requires both T: Send and T: Sync in order for RwLock<T> itself to be Sync:
| Lock Type | Bound Required for Lock to Implement Sync |
Reason |
|---|---|---|
Mutex<T> |
T: Send (does not require T: Sync) |
Only one thread ever accesses &T or &mut T at a time |
RwLock<T> |
T: Send + Sync |
Multiple threads can hold RwLockReadGuard (&T) simultaneously |
For example, Mutex<RefCell<i32>> is Sync (safe to share via Arc), whereas RwLock<RefCell<i32>> is not Sync because concurrent readers could call RefCell::borrow_mut() from different threads and cause a data race.
Poisoning Rules in RwLock<T>
An RwLock<T> becomes poisoned only if a thread panics while holding an exclusive RwLockWriteGuard<'_, T>. A panic inside a thread holding only a shared RwLockReadGuard<'_, T> cannot corrupt T's invariants and therefore does not poison the lock.