Smart Pointers

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.