NAME

Acme::Parataxis::Sync::RwLock - Writer-Preferring Reader-Writer Lock for Fibers

SYNOPSIS

use v5.40;
use Acme::Parataxis qw[async fiber await_sleep];
use Acme::Parataxis::Sync::RwLock;

my $rwlock = Acme::Parataxis::Sync::RwLock->new;
my %cache  = ( a => 1, b => 2 );

async {
    # Concurrent readers share the lock simultaneously:
    my @readers = map {
        my $id = $_;
        fiber {
            my $guard = $rwlock->read_guard;
            say "Reader $id active; concurrent read holds: " . $rwlock->readers;
            await_sleep(20);
        };
    } 1 .. 3;

    # Writer claims exclusive access; queues behind current readers
    # but blocks all subsequent readers from jumping ahead:
    my $writer = fiber {
        await_sleep(5);
        my $guard = $rwlock->write_guard;
        say "Writer active (exclusive); updating cache...";
        $cache{c} = 3;
    };

    $_->await for @readers, $writer;
    say "Cache updated safely: " . keys(%cache) . " items";
};

DESCRIPTION

Acme::Parataxis::Sync::RwLock provides a shared-reader, exclusive-writer lock for coordinating fibers. Multiple fibers may acquire concurrent read holds, but a write lock is strictly exclusive: no other reader or writer may enter while a write hold is active.

Writer Preference

In a naive reader-writer lock, a continuous stream of readers can starve a queued writer indefinitely. Acme::Parataxis::Sync::RwLock enforces writer preference:

  • The moment a writer requests a lock (calling write_lock or write_guard), any subsequent reader arriving at the lock is placed in the wait queue behind that writer.

  • Existing active readers are allowed to drain naturally. Once the last reader finishes, the queued writer is granted the lock immediately.

  • When the writer releases, all readers queued behind it are unblocked together in a single pass to share the lock.

Re-entrant Read Holds

A fiber that already owns a read hold may take additional read holds (deepening the lock) without deadlocking. The lock tracks hold counts per fiber, allowing helper functions called within a read scope to acquire their own read guards safely.

METHODS

new( )

my $lock = Acme::Parataxis::Sync::RwLock->new;

Constructs a new, unlocked reader-writer lock. Takes no parameters.

read_lock( )

$lock->read_lock;

Acquires a shared read hold. Parks the calling fiber if an exclusive write lock is active or if a writer is currently queued ahead in line.

If the calling fiber already holds a read hold, the hold count is incremented in place without parking. If the calling fiber holds the exclusive write lock, it is granted a read hold immediately (since it already holds exclusive access). Returns true.

read_unlock( )

$lock->read_unlock;

Releases one read hold for the calling fiber. If this was the last active read hold, calls the internal scheduler pump to hand the lock to the next queued writer. Croaks if the calling fiber holds no active read holds. Returns true.

try_read_lock( )

if ( $lock->try_read_lock ) {
    # read hold acquired without parking
} else {
    # writer active or writer queued ahead
}

Attempts to acquire a read hold immediately without parking. Returns true on success. Returns false if an exclusive writer is active or if a writer is currently queued. If the calling fiber already holds a read hold, deepens the hold and returns true.

write_lock( )

$lock->write_lock;

Acquires the exclusive write lock. Parks the calling fiber until all active read holds have drained and no earlier writers are queued ahead.

  • Croaks if the calling fiber already holds the write lock (non-reentrant).

  • Croaks if the calling fiber holds an active read hold (upgrades are not supported).

Returns true once the write lock is granted.

write_unlock( )

$lock->write_unlock;

Releases the exclusive write lock. Hands the lock to the next queued writer, or unblocks all queued readers if no writers are waiting. Croaks if the lock is not write-held, or if the calling fiber is not the current writer. Returns true.

try_write_lock( )

Attempts to acquire the write lock without parking. Returns true on success. Returns false if any reader is active, a writer is active, or an earlier writer is queued. Croaks on self-reentrancy or if the caller holds a read hold.

read_guard( )

{
    my $guard = $lock->read_guard;
    # read-protected scope
}

Acquires a read hold via read_lock() and returns an opaque guard object. When the guard goes out of scope (or is unwound by an exception), it invokes read_unlock() automatically.

write_guard( )

{
    my $guard = $lock->write_guard;
    # write-protected scope
}

Acquires the exclusive write lock via write_lock() and returns an opaque guard object. When the guard is destroyed, it invokes write_unlock() automatically.

readers( )

Returns the total count of outstanding read holds across all fibers (including deepened holds).

read_holders( )

Returns the count of distinct fibers currently holding at least one read hold.

writer( ) / owner( )

Returns the numeric fiber ID of the current exclusive writer, or undef if not write-held. owner is an alias.

waiters( )

Returns the total number of fibers currently parked waiting for either read or write access.

read_waiters( ) / write_waiters( )

In scalar context, returns the count of fibers waiting for read or write access, respectively. In list context, returns their numeric fiber IDs. Wrap in scalar() when interpolating into strings:

say "Queued readers: " . scalar($lock->read_waiters);

Recipes

Nested read calls inside helpers

my $rwlock = Acme::Parataxis::Sync::RwLock->new;

sub fetch_field ($key) {
    my $guard = $rwlock->read_guard;    # deepens existing hold safely
    return $data{$key};
}

async {
    my $outer_guard = $rwlock->read_guard;
    my $val1 = fetch_field('name');
    my $val2 = fetch_field('role');
};

Deadlock prevention during writes

Because upgrading a read hold to a write hold is forbidden, release read holds before requesting a write lock:

sub update_record ($id, $new_val) {
    # Check current value under read lock:
    my $needs_update = 0;
    {
        my $guard = $rwlock->read_guard;
        $needs_update = 1 if $data{$id} ne $new_val;
    }

    # Drop read hold before requesting write lock:
    if ($needs_update) {
        my $guard = $rwlock->write_guard;
        $data{$id} = $new_val;
    }
}

Behavior Notes

  • Upgrades from read to write are forbidden

    Attempting to call write_lock() while holding a read hold croaks with RwLock write_lock: this fiber holds a read lock; upgrading read to write is not supported. Upgrading cannot be supported safely without risking deadlocks when multiple readers attempt to upgrade concurrently. Release the read hold first.

  • Stranded holds brick the lock

    If a fiber exits without unlocking (or without dropping a guard), its holds remain registered permanently. For write holds, this blocks all future access. For read holds, recycled fiber IDs can cause a newer fiber with the same ID to be falsely identified as holding a read hold. Always use guards.

SEE ALSO

Acme::Parataxis::Sync, Acme::Parataxis::Sync::Mutex, Acme::Parataxis::Sync::WaitGroup

AUTHOR

Sanko Robinson https://github.com/sanko

LICENSE

Copyright (C) Sanko Robinson.

This library is free software; you can redistribute it and/or modify it under the terms found in the Artistic License 2.