๐Ÿฆ€ Functional Rust

987: Read-Write Lock Pattern

Difficulty: Intermediate Category: Async / Concurrency FP Patterns Concept: Allow many concurrent readers OR one exclusive writer Key Insight: `RwLock<T>` is `Mutex<T>` with two lock modes; use for read-heavy workloads where reads vastly outnumber writes โ€” multiple threads can read in parallel without blocking each other

Versions

DirectoryDescription
`std/`Standard library version using `std::sync`, `std::thread`
`tokio/`Tokio async runtime version using `tokio::sync`, `tokio::spawn`

Running

# Standard library version
cd std && cargo test

# Tokio version
cd tokio && cargo test
// 987: Read-Write Lock Pattern
// Rust: RwLock<T> โ€” many readers OR one writer, never both

use std::sync::{Arc, RwLock};
use std::thread;

// --- Approach 1: Multiple readers in parallel ---
fn concurrent_readers() -> Vec<i32> {
    let data = Arc::new(RwLock::new(42i32));

    let handles: Vec<_> = (0..5).map(|_| {
        let data = Arc::clone(&data);
        thread::spawn(move || {
            let guard = data.read().unwrap(); // shared read lock
            *guard // all 5 can hold read lock simultaneously
        })
    }).collect();

    handles.into_iter().map(|h| h.join().unwrap()).collect()
}

// --- Approach 2: Writer excludes readers ---
fn write_then_read() -> i32 {
    let data = Arc::new(RwLock::new(0i32));

    {
        let mut guard = data.write().unwrap(); // exclusive write lock
        *guard = 100;
        // guard drops here โ€” write lock released
    }

    let guard = data.read().unwrap();
    *guard
}

// --- Approach 3: Shared config pattern (read-heavy) ---
#[derive(Clone, Debug)]
struct Config {
    threshold: i32,
    name: String,
}

fn config_pattern() -> (String, i32) {
    let config = Arc::new(RwLock::new(Config {
        threshold: 10,
        name: "default".to_string(),
    }));

    // Many readers
    let readers: Vec<_> = (0..4).map(|_| {
        let config = Arc::clone(&config);
        thread::spawn(move || {
            let c = config.read().unwrap();
            (c.name.clone(), c.threshold)
        })
    }).collect();

    // One writer updates the config
    {
        let cfg = Arc::clone(&config);
        let writer = thread::spawn(move || {
            let mut c = cfg.write().unwrap();
            c.threshold = 99;
            c.name = "updated".to_string();
        });
        writer.join().unwrap();
    }

    for h in readers { h.join().unwrap(); } // let readers finish

    let c = config.read().unwrap();
    (c.name.clone(), c.threshold)
}

fn main() {
    let reads = concurrent_readers();
    println!("concurrent readers: {:?}", reads);

    let v = write_then_read();
    println!("write then read: {}", v);

    let (name, threshold) = config_pattern();
    println!("config after update: name={} threshold={}", name, threshold);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_concurrent_readers_all_see_same() {
        let reads = concurrent_readers();
        assert_eq!(reads.len(), 5);
        assert!(reads.iter().all(|&v| v == 42));
    }

    #[test]
    fn test_write_then_read() {
        assert_eq!(write_then_read(), 100);
    }

    #[test]
    fn test_config_pattern() {
        let (name, threshold) = config_pattern();
        assert_eq!(name, "updated");
        assert_eq!(threshold, 99);
    }

    #[test]
    fn test_try_read_write() {
        let rw = RwLock::new(0i32);
        let _r1 = rw.read().unwrap();
        let _r2 = rw.read().unwrap(); // multiple reads OK
        // rw.try_write() would fail here (readers active)
        assert!(rw.try_write().is_err());
    }

    #[test]
    fn test_rwlock_write_exclusive() {
        let rw = Arc::new(RwLock::new(vec![1, 2, 3]));
        {
            let mut w = rw.write().unwrap();
            w.push(4);
        }
        assert_eq!(*rw.read().unwrap(), vec![1, 2, 3, 4]);
    }
}
(* 987: Read-Write Lock Pattern *)
(* OCaml: Simulated RwLock using reader count + writer mutex *)

(* --- RwLock simulation: multiple readers OR one writer --- *)

type 'a rwlock = {
  mutable data: 'a;
  mutable readers: int;
  m: Mutex.t;
  can_write: Condition.t;
  can_read: Condition.t;
  mutable writer_waiting: bool;
}

let make_rwlock v = {
  data = v;
  readers = 0;
  m = Mutex.create ();
  can_write = Condition.create ();
  can_read = Condition.create ();
  writer_waiting = false;
}

let read_lock rw =
  Mutex.lock rw.m;
  while rw.writer_waiting do
    Condition.wait rw.can_read rw.m
  done;
  rw.readers <- rw.readers + 1;
  Mutex.unlock rw.m

let read_unlock rw =
  Mutex.lock rw.m;
  rw.readers <- rw.readers - 1;
  if rw.readers = 0 then Condition.signal rw.can_write;
  Mutex.unlock rw.m

let write_lock rw =
  Mutex.lock rw.m;
  rw.writer_waiting <- true;
  while rw.readers > 0 do
    Condition.wait rw.can_write rw.m
  done

let write_unlock rw =
  rw.writer_waiting <- false;
  Condition.broadcast rw.can_read;
  Mutex.unlock rw.m

let with_read rw f =
  read_lock rw;
  let result = (try f rw.data with e -> read_unlock rw; raise e) in
  read_unlock rw;
  result

let with_write rw f =
  write_lock rw;
  (try f rw with e -> write_unlock rw; raise e);
  write_unlock rw

(* --- Approach 1: Multiple readers, no conflict --- *)

let () =
  let rw = make_rwlock 42 in
  (* Multiple concurrent readers *)
  let threads = List.init 5 (fun _ ->
    Thread.create (fun () ->
      let v = with_read rw (fun x -> x) in
      assert (v = 42)
    ) ()
  ) in
  List.iter Thread.join threads;
  Printf.printf "Approach 1 (multiple readers): all read %d\n" 42

(* --- Approach 2: Writer updates, readers see new value --- *)

let () =
  let rw = make_rwlock 0 in

  let writer = Thread.create (fun () ->
    with_write rw (fun rw -> rw.data <- 100)
  ) () in
  Thread.join writer;

  let v = with_read rw (fun x -> x) in
  assert (v = 100);
  Printf.printf "Approach 2 (writer then reader): %d\n" v

let () = Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Read-Write Lock Pattern โ€” Comparison

Core Insight

RwLock encodes the read-write exclusion invariant in the type: `&T` access (shared) maps to read lock; `&mut T` access (exclusive) maps to write lock. This mirrors Rust's own ownership model.

OCaml Approach

  • No standard RwLock โ€” must simulate with `Mutex` + `Condition` + reader count
  • `readers: int ref` tracks active readers; writer waits until `readers = 0`
  • `writer_waiting: bool` prevents reader starvation of writers
  • More complex than needed โ€” OCaml's GC handles most sharing without locks

Rust Approach

  • `RwLock::new(data)` is standard in `std::sync`
  • `rw.read()` โ†’ `RwLockReadGuard` โ€” shared, many at once
  • `rw.write()` โ†’ `RwLockWriteGuard` โ€” exclusive, blocks all others
  • `try_read()` / `try_write()` non-blocking variants
  • RAII: guards unlock on drop โ€” no manual unlock

Comparison Table

ConceptOCaml (simulated)Rust
CreateManual struct + Mutex + Condition`RwLock::new(data)`
Read lock`read_lock` / `read_unlock``rw.read().unwrap()`
Write lock`write_lock` / `write_unlock``rw.write().unwrap()`
Multiple readersYes (via reader count)Yes โ€” `RwLockReadGuard` is shared
Prevent writer starvationManual `writer_waiting` flagImplementation-dependent
UnlockManual callDrop the guard (RAII)
Try-lockNot shown (custom needed)`try_read()` / `try_write()`

std vs tokio

Aspectstd versiontokio version
RuntimeOS threads via `std::thread`Async tasks on tokio runtime
Synchronization`std::sync::Mutex`, `Condvar``tokio::sync::Mutex`, channels
Channels`std::sync::mpsc` (unbounded)`tokio::sync::mpsc` (bounded, async)
BlockingThread blocks on lock/recvTask yields, runtime switches tasks
OverheadOne OS thread per taskMany tasks per thread (M:N)
Best forCPU-bound, simple concurrencyI/O-bound, high-concurrency servers