NAME
    Data::Sync::Shared - Shared-memory synchronization primitives for Linux

SYNOPSIS
        use Data::Sync::Shared;

        # Semaphore -- bounded counter for resource limiting
        my $sem = Data::Sync::Shared::Semaphore->new('/tmp/sem.shm', 4);
        $sem->acquire;            # block until available
        $sem->acquire(1.5);       # with timeout
        $sem->try_acquire;        # non-blocking
        $sem->acquire_n(3);       # acquire N permits atomically
        $sem->release;
        $sem->release(2);         # release N permits
        my $n = $sem->drain;      # acquire all, return count
        {
            my $g = $sem->acquire_guard;   # auto-release on scope exit
        }

        # Barrier -- N processes rendezvous
        my $bar = Data::Sync::Shared::Barrier->new('/tmp/bar.shm', 3);
        my $leader = $bar->wait;       # block until all 3 arrive
        my $leader = $bar->wait(5.0);  # with timeout (-1=timeout)

        # RWLock -- reader-writer lock
        my $rw = Data::Sync::Shared::RWLock->new('/tmp/rw.shm');
        $rw->rdlock;  $rw->rdunlock;
        $rw->wrlock;  $rw->wrunlock;
        $rw->try_rdlock;  $rw->try_wrlock;
        $rw->downgrade;                # wrlock -> rdlock atomically
        {
            my $g = $rw->wrlock_guard;  # auto-release on scope exit
        }

        # Condvar -- condition variable with built-in mutex
        my $cv = Data::Sync::Shared::Condvar->new('/tmp/cv.shm');
        $cv->lock;
        $cv->try_lock;       # non-blocking
        $cv->wait;           # atomically unlock + wait + re-lock
        $cv->wait(2.0);      # with timeout
        $cv->signal;         # wake one waiter
        $cv->broadcast;      # wake all waiters
        $cv->wait_while(sub { !$ready }, 5.0);  # predicate loop
        $cv->unlock;

        # Once -- one-time initialization gate
        my $once = Data::Sync::Shared::Once->new('/tmp/once.shm');
        if ($once->enter) {          # or enter($timeout)
            do_init();
            $once->done;
        }

        # All primitives support anonymous (fork-inherited) mode:
        my $sem = Data::Sync::Shared::Semaphore->new(undef, 4);

        # And memfd mode (fd-passable):
        my $sem = Data::Sync::Shared::Semaphore->new_memfd("my_sem", 4);
        my $fd = $sem->memfd;

DESCRIPTION
    Data::Sync::Shared provides five cross-process synchronization primitives
    stored in file-backed shared memory (mmap(MAP_SHARED)), using Linux futex
    for efficient blocking.

    Linux-only. Requires 64-bit Perl.

  Primitives
    Data::Sync::Shared::Semaphore - bounded counter
        CAS-based counting semaphore. "acquire" decrements (blocks at 0),
        "release" increments (capped at max). Useful for cross-process
        resource limiting (connection pools, worker slots).

    Data::Sync::Shared::Barrier - rendezvous point
        N processes call "wait"; all block until the last one arrives, then
        all proceed. Returns true for one "leader" process. Generation counter
        tracks how many times the barrier has tripped.

    Data::Sync::Shared::RWLock - reader-writer lock
        Multiple concurrent readers or one exclusive writer. Readers use
        "rdlock"/"rdunlock", writers use "wrlock"/"wrunlock". Non-blocking
        "try_rdlock"/"try_wrlock" variants available.

    Data::Sync::Shared::Condvar - condition variable
        Includes a built-in mutex. "lock"/"unlock" protect the predicate.
        "wait" atomically releases the mutex and sleeps; on wakeup it
        re-acquires the mutex. "signal" wakes one waiter, "broadcast" wakes
        all.

    Data::Sync::Shared::Once - one-time init gate
        "enter" returns true for exactly one process (the initializer); all
        others block until "done" is called. If the initializer dies, waiters
        detect the stale PID and a new initializer is elected.

  Features
    *   File-backed mmap for cross-process sharing

    *   Futex-based blocking (no busy-spin, no pthread)

    *   PID-based stale lock recovery (dead process detection)

    *   Anonymous and memfd modes

    *   Timeouts on all blocking operations (except Condvar "lock", which
        provides only a non-blocking "try_lock")

    *   eventfd integration for event-loop wakeup

  Crash Safety
    All primitives encode the holder's PID in the lock word. If a process dies
    while holding a lock, other processes detect the stale lock within 2
    seconds via "kill(pid, 0)" and automatically recover.

    Reader-slot exhaustion (slotless readers): dead-process recovery
    attributes a crashed lock holder's contribution through its reader-slot.
    The slot table holds 1024 entries (one per concurrent reader process). If
    more than that many reader processes share one mapping at once, a reader
    that cannot claim a slot proceeds "slotless" -- it still takes the read
    lock but leaves no per-process record. If such a slotless reader is then
    killed while holding the read lock, its share of the lock cannot be
    attributed to a dead process, so writer recovery cannot reclaim it and
    writers may block until the mapping is recreated. Reaching this needs more
    than 1024 concurrent reader processes on one mapping plus a crash in the
    brief read-lock window; the dead-process slot reclaim keeps the table from
    filling with stale entries, so in practice it is very unlikely.

  Security
    Backing files are created securely: the path is opened with
    O_CREAT|O_EXCL|O_NOFOLLOW (a pre-existing symlink at the path is rejected,
    and an existing regular file is attached rather than truncated) and the
    new file is created mode 0600 (owner-only) by default, so a segment is not
    world-writable unless you opt in. To share a file with a peer group, pass
    an explicit octal mode as the last argument to new():

        my $sem = Data::Sync::Shared::Semaphore->new($path, $max, $initial, 0660);
        my $rw  = Data::Sync::Shared::RWLock->new($path, 0660);

    The mode is applied exactly via "fchmod" after an "O_EXCL" create, so the
    caller's umask does not narrow it. Offsets read back from an attached
    segment are bounds-checked before use, so a poisoned reader-slot offset
    cannot steer a pointer outside the mapping.

  Guard Objects
    All locking primitives provide scope-based guards that auto-release on
    scope exit (including exceptions):

        {
            my $g = $rw->rdlock_guard;
            # ... read operations ...
        }  # rdunlock called automatically

        {
            my $g = $sem->acquire_guard(3);  # acquire 3 permits
            # ... use resource ...
        }  # release(3) called automatically

        {
            my $g = $cv->lock_guard;
            $cv->wait_while(sub { !$ready }, 5.0);
        }  # unlock called automatically

PRIMITIVES
  Data::Sync::Shared::Semaphore
   Constructors
        my $sem = Data::Sync::Shared::Semaphore->new($path, $max);
        my $sem = Data::Sync::Shared::Semaphore->new($path, $max, $initial);
        my $sem = Data::Sync::Shared::Semaphore->new(undef, $max);
        my $sem = Data::Sync::Shared::Semaphore->new_memfd($name, $max);
        my $sem = Data::Sync::Shared::Semaphore->new_memfd($name, $max, $initial);
        my $sem = Data::Sync::Shared::Semaphore->new_from_fd($fd);

    $max is the maximum permit count. $initial defaults to $max (fully
    available); set to 0 to start with no available permits.

   Operations
        my $ok  = $sem->acquire;              # block until available (infinite)
        my $ok  = $sem->acquire($timeout);    # block with timeout (seconds)
        my $ok  = $sem->try_acquire;          # non-blocking, false if unavailable
        my $ok  = $sem->acquire_n($n);        # acquire N permits atomically
        my $ok  = $sem->acquire_n($n, $timeout);
        my $ok  = $sem->try_acquire_n($n);    # non-blocking N-permit acquire
        $sem->release;                        # release one permit
        $sem->release($n);                    # release N permits (clamped at max)
        my $n   = $sem->drain;               # acquire all available, return count
        my $val = $sem->value;                # current available count
        my $max = $sem->max;                  # maximum permits

   Guard
        my $g = $sem->acquire_guard;          # acquire 1, release on scope exit
        my $g = $sem->acquire_guard($n);      # acquire N
        my $g = $sem->acquire_guard($n, $timeout);  # with timeout, undef on fail

  Data::Sync::Shared::Barrier
   Constructors
        my $bar = Data::Sync::Shared::Barrier->new($path, $parties);
        my $bar = Data::Sync::Shared::Barrier->new(undef, $parties);
        my $bar = Data::Sync::Shared::Barrier->new_memfd($name, $parties);
        my $bar = Data::Sync::Shared::Barrier->new_from_fd($fd);

    $parties must be >= 2.

   Operations
        my $r = $bar->wait;           # block until all parties arrive
        my $r = $bar->wait($timeout); # with timeout

    Returns: 1 = leader (last to arrive), 0 = non-leader, -1 = timeout. A zero
    timeout (wait(0)) is a non-blocking probe: it returns -1 if the party
    would have to block, without breaking the barrier. On a positive timeout
    that actually elapses the barrier is permanently broken: "wait" returns -1
    for the timing-out party and all other waiters, and every subsequent
    "wait" also returns -1 until "reset" is called. This matches POSIX
    "pthread_barrier_t" broken semantics.

        my $gen = $bar->generation;    # how many times barrier has tripped
        my $n   = $bar->arrived;       # currently arrived count
        my $n   = $bar->parties;       # party count
        $bar->is_broken;               # true if timeout broke the barrier
        $bar->reset;                   # clear broken state + bump generation

  Data::Sync::Shared::RWLock
   Constructors
        my $rw = Data::Sync::Shared::RWLock->new($path);
        my $rw = Data::Sync::Shared::RWLock->new(undef);
        my $rw = Data::Sync::Shared::RWLock->new_memfd($name);
        my $rw = Data::Sync::Shared::RWLock->new_from_fd($fd);

   Operations
        $rw->rdlock;                   # block until read lock acquired
        $rw->rdlock($timeout);         # with timeout (croaks on timeout)
        $rw->wrlock;                   # block until write lock acquired
        $rw->wrlock($timeout);         # with timeout (croaks on timeout)
        my $ok = $rw->try_rdlock;      # non-blocking
        my $ok = $rw->try_wrlock;      # non-blocking
        my $ok = $rw->rdlock_timed($timeout);  # returns false on timeout
        my $ok = $rw->wrlock_timed($timeout);  # returns false on timeout
        $rw->rdunlock;
        $rw->wrunlock;
        $rw->downgrade;                # convert wrlock to rdlock atomically

   Guards
        my $g = $rw->rdlock_guard;             # rdunlock on scope exit
        my $g = $rw->rdlock_guard($timeout);   # with timeout (croaks on fail)
        my $g = $rw->wrlock_guard;
        my $g = $rw->wrlock_guard($timeout);

  Data::Sync::Shared::Condvar
   Constructors
        my $cv = Data::Sync::Shared::Condvar->new($path);
        my $cv = Data::Sync::Shared::Condvar->new(undef);
        my $cv = Data::Sync::Shared::Condvar->new_memfd($name);
        my $cv = Data::Sync::Shared::Condvar->new_from_fd($fd);

   Operations
        $cv->lock;                     # acquire built-in mutex
        $cv->unlock;                   # release built-in mutex
        my $ok = $cv->try_lock;        # non-blocking

        my $ok = $cv->wait;            # unlock, wait for signal, re-lock
        my $ok = $cv->wait($timeout);  # with timeout
        $cv->signal;                   # wake one waiter
        $cv->broadcast;                # wake all waiters

        my $ok = $cv->wait_while(\&pred);           # loop until pred returns false
        my $ok = $cv->wait_while(\&pred, $timeout); # with timeout

    "wait" must be called while holding the mutex. Returns 1 on
    signal/broadcast, 0 on timeout. The mutex is always re-acquired before
    "wait" returns.

    "wait_while" calls "wait" in a loop until the predicate coderef returns
    false. Returns 1 if predicate became false, 0 on timeout.

   Guard
        my $g = $cv->lock_guard;       # unlock on scope exit

  Data::Sync::Shared::Once
   Constructors
        my $once = Data::Sync::Shared::Once->new($path);
        my $once = Data::Sync::Shared::Once->new(undef);
        my $once = Data::Sync::Shared::Once->new_memfd($name);
        my $once = Data::Sync::Shared::Once->new_from_fd($fd);

   Operations
        my $init = $once->enter;             # try + wait, infinite
        my $init = $once->enter($timeout);   # with timeout
        $once->done;                         # mark initialization complete
        my $ok  = $once->is_done;            # check without blocking
        $once->reset;                        # reset to uninitialized state

    "enter" returns true for exactly one process (the initializer). All others
    block until "done" is called, then return false. With a $timeout, a waiter
    that times out before initialization completes also returns false: a false
    return means "not the initializer", not necessarily "initialization
    complete", so use "is_done" to tell a finished gate from a timed-out wait.
    If the initializer dies, stale PID detection elects a new one.

  Common Methods
    All primitives support:

        my $p  = $obj->path;           # backing file path (undef if anon)
        my $fd = $obj->memfd;          # memfd fd (-1 if file-backed/anon)
        $obj->sync;                    # msync -- flush to disk
        $obj->unlink;                  # remove backing file
        Class->unlink($path);          # class method form
        my $s  = $obj->stats;          # diagnostic hashref

    Stats keys vary by type. All counters are approximate under concurrency.

    Semaphore: "value", "max", "waiters", "mmap_size", "acquires", "releases",
    "waits", "timeouts", "recoveries".

    Barrier: "parties", "arrived", "generation", "waiters", "mmap_size",
    "waits", "releases", "timeouts".

    RWLock: "state" ("unlocked", "read_locked", "write_locked"), "readers",
    "waiters", "mmap_size", "acquires", "releases", "recoveries".

    Condvar: "waiters", "signals", "mmap_size", "acquires", "releases",
    "waits", "timeouts", "recoveries".

    Once: "state" ("init", "running", "done"), "is_done", "waiters",
    "mmap_size", "acquires", "releases", "waits", "timeouts", "recoveries".

   eventfd Integration
        my $fd = $obj->eventfd;        # create eventfd, returns fd
        $obj->eventfd_set($fd);        # use existing fd (e.g. from fork)
        my $fd = $obj->fileno;         # current eventfd (-1 if none)
        $obj->notify;                  # signal eventfd
        my $n  = $obj->eventfd_consume;  # drain notification counter

    Notification is opt-in. Use with EV or other event loops.

CRASH SAFETY
    An interrupted create is recovered too. A creator killed after the backing
    file is sized but before its header is committed leaves a full-size,
    all-zero file. "new" re-initializes such a file automatically, but only
    when it is exactly the size the requested geometry needs, is owned by your
    effective uid, and is still entirely zero -- a file holding data is never
    re-initialized. If the creator got as far as writing part of the header,
    the file cannot be told apart from a corrupt one and "new" croaks with
    "incomplete sync file left by an interrupted create; remove it and retry".
    A file left behind by an interrupted create never held data, so removing
    it is safe -- but a file whose header was corrupted after the fact reaches
    the same croak, so confirm it is an abandoned create before deleting
    anything you care about.

SECURITY
    Backing files are created with mode 0600 (owner-only) by default, so only
    the creating user can open and attach them. To share a backing file across
    users, pass an explicit octal file mode such as 0660 as the last argument
    to "new" -- "Semaphore->new($path, $max, $initial, $mode)",
    "Barrier->new($path, $parties, $mode)", "RWLock->new($path, $mode)",
    "Condvar->new($path, $mode)" and "Once->new($path, $mode)". The mode is
    applied when the file is created, and when a file left behind by an
    interrupted create is re-initialized (see "CRASH SAFETY"); a file already
    in use keeps its own permissions. The file is opened with "O_NOFOLLOW", so
    a symlink planted at the path is refused, and created with "O_EXCL"; the
    on-disk header is validated when the file is attached. Any process you
    grant write access to a shared mapping is trusted not to corrupt its
    contents while other processes are using it -- a primitive whose header is
    rewritten underneath it can leave waiters parked, which no amount of
    validation at attach time can undo.

SEE ALSO
    Data::Buffer::Shared - typed shared array

    Data::HashMap::Shared - concurrent hash table

    Data::Queue::Shared - FIFO queue

    Data::PubSub::Shared - publish-subscribe ring

    Data::ReqRep::Shared - request-reply

    Data::Pool::Shared - fixed-size object pool

    Data::Stack::Shared - LIFO stack

    Data::Deque::Shared - double-ended queue

    Data::Log::Shared - append-only log (WAL)

    Data::Heap::Shared - priority queue

    Data::Graph::Shared - directed weighted graph

    Data::BitSet::Shared - shared bitset (lock-free per-bit ops)

    Data::RingBuffer::Shared - fixed-size overwriting ring buffer

AUTHOR
    vividsnow

LICENSE
    This is free software; you can redistribute it and/or modify it under the
    same terms as Perl itself.

