Synchronization Primitives

RwLock<T>

std::sync::RwLock<T> (Reader-Writer Lock) is a synchronization primitive that distinguishes read-only access from mutating write access: any number of threads may hold shared read locks (RwLockReadGuard<'_, T>) simultaneously, whereas at most one thread may hold an exclusive write lock (RwLockWriteGuard<'_, T>) with zero active readers.

Reader-Writer State Machine

RwLock<T> Internal State Word:

  [ Unlocked (readers = 0, writer = false) ]
        |                          |
        | .read()                  | .write()
        v                          v
  [ Shared Read Mode ]       [ Exclusive Write Mode ]
    - readers >= 1             - writer = true, readers = 0
    - New `.read()` calls      - All `.read()` and `.write()`
      succeed concurrently*      calls block until writer guard drops
    - `.write()` blocks until
      readers drop to 0

*Note: To prevent writer starvation on modern operating systems, once a thread blocks waiting on .write(), subsequent .read() calls queue behind the pending writer so a continuous stream of overlapping readers cannot lock out writers forever.

Complete RwLock<T> API Reference

Method Signature Description
RwLock::new(val) const fn new(t: T) -> RwLock<T> Creates an unlocked RwLock<T> (usable in static)
.read(&self) fn read(&self) -> LockResult<RwLockReadGuard<'_, T>> Blocks until no writer is active and returns a shared read guard (&T)
.try_read(&self) fn try_read(&self) -> TryLockResult<RwLockReadGuard<'_, T>> Non-blocking attempt to acquire a shared read guard
.write(&self) fn write(&self) -> LockResult<RwLockWriteGuard<'_, T>> Blocks until all readers and writers finish and returns an exclusive write guard (&mut T)
.try_write(&self) fn try_write(&self) -> TryLockResult<RwLockWriteGuard<'_, T>> Non-blocking attempt to acquire an exclusive write guard
.get_mut(&mut self) fn get_mut(&mut self) -> LockResult<&mut T> Zero-cost &mut T access when caller holds exclusive &mut RwLock<T>
.into_inner(self) fn into_inner(self) -> LockResult<T> Consumes the RwLock<T> and returns T

Read-Heavy Configuration Cache Example

RwLock<T> outperforms Mutex<T> when reads vastly outnumber writes and each read operation does enough work (such as searching a multi-element structure or validating rules) that concurrent readers overlap significantly:

use std::collections::BTreeMap;
use std::sync::RwLock;
use std::thread;

struct FeatureFlags {
    flags: RwLock<BTreeMap<String, bool>>,
}

impl FeatureFlags {
    fn new() -> Self {
        Self {
            flags: RwLock::new(BTreeMap::from([
                (String::from("dark_mode"), true),
                (String::from("beta_search"), false),
            ])),
        }
    }

    fn is_enabled(&self, key: &str) -> bool {
        // Multiple threads execute `.read()` concurrently without blocking each other:
        self.flags.read().unwrap().get(key).copied().unwrap_or(false)
    }

    fn set_flag(&self, key: &str, enabled: bool) {
        // Exclusive `.write()` waits for active readers and updates the map:
        self.flags.write().unwrap().insert(key.to_string(), enabled);
    }
}

fn main() {
    let store = FeatureFlags::new();

    thread::scope(|s| {
        for reader_id in 0..3 {
            let store_ref = &store;
            s.spawn(move || {
                println!(
                    "reader #{reader_id}: dark_mode = {}",
                    store_ref.is_enabled("dark_mode")
                );
            });
        }

        s.spawn(|| {
            store.set_flag("beta_search", true);
        });
    });

    println!("after update: beta_search = {}", store.is_enabled("beta_search"));
}
reader #0: dark_mode = true
reader #1: dark_mode = true
reader #2: dark_mode = true
after update: beta_search = true

When Mutex<T> Is Actually Faster Than RwLock<T>

If the critical section is tiny (such as incrementing two integers or pushing a pointer onto a Vec), Mutex<T> is often faster than RwLock<T> even on read-mostly workloads because RwLock::read() still executes an atomic read-modify-write instruction on the shared reader counter (causing cache-line bouncing across CPU cores) and has more complex bookkeeping than Mutex<T>.