Skip to content

CONCEPT Cited by 1 source

Read-recursive lock

A read-recursive lock is a RwLock acquisition API that lets a thread acquire a read lock even if it already holds one, bypassing the writer-preference starvation avoidance logic that would normally block re-entrant reads when a writer is waiting.

Rust's parking_lot exposes this as RwLock::read_recursive().

The primitive exists because writer preference has a cost

Most RwLock implementations prefer writers to prevent starvation — if a reader holds the lock and a writer is waiting, new readers block rather than piling on (because otherwise an unlucky writer could wait forever behind a continuous stream of readers).

Re-entrance breaks under this rule. Consider:

fn process(lock: &RwLock<State>) {
    let guard = lock.read();                 // reader 1
    if guard.needs_deep_lookup() {
        inner_lookup(lock);                  // wants another read
    }
}
fn inner_lookup(lock: &RwLock<State>) {
    let _ = lock.read();                     // blocks if writer queued
}

If a writer is queued between the two reads, inner_lookup blocks. The current thread is now holding a read lock and blocked waiting for another read lock from the same thread. Self-deadlock against the starvation-avoidance rule.

read_recursive gives inner_lookup a way out: "I already hold a read lock on this very RwLock, let me have another one regardless of the writer-preference rule."

Ptacek's rhetorical framing

"A recursive read lock is an eldritch rite invoked when you need to grab a read lock deep in a call tree where you already grabbed that lock, but can't be arsed to structure the code properly to reflect that." (Source: sources/2025-05-28-flyio-parking-lot-ffffffffffffffff)

The proper fix is normally to restructure the code so a single read guard covers the whole operation. read_recursive is the escape hatch for codebases where that restructuring is expensive or where re-entrance is unavoidable (e.g. callbacks, trait implementations that take a different path under some conditions).

Used as a desperation probe

In the Fly.io 2025-05-28 parking_lot bug, Fly switched their Catalog reads to read_recursive not because the code was re-entrant — it wasn't — but as a probe: maybe some slow reader was poisoning the lock in a way that writer-preference was mis-handling. The probe didn't fix the bug (because the bug wasn't in writer-preference logic — it was a bitwise double-free in the clear-bits atomic path), but it did produce a new error message (RwLock reader count overflow) that was the first direct evidence of lock-word corruption. See patterns/read-recursive-as-desperation-probe.

Risks

  • Writer starvation: aggressive use of read_recursive reintroduces the starvation risk that writer-preference solves. Each recursive acquire bypasses the starvation- avoidance; a long read-heavy call graph can indefinitely delay waiting writers.
  • Code-structure debt: using read_recursive instead of hoisting the read guard to cover the whole call graph hides re-entrance from the type system.
  • Deadlock through writes can still happen: a re-entrant write lock (not covered by read_recursive) still self-deadlocks normally.

Seen in

Last updated · 200 distilled / 1,178 read