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. 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.