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
-
CAS-based counting semaphore.
acquiredecrements (blocks at 0),releaseincrements (capped at max). Useful for cross-process resource limiting (connection pools, worker slots). -
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. -
Multiple concurrent readers or one exclusive writer. Readers use
rdlock/rdunlock, writers usewrlock/wrunlock. Non-blockingtry_rdlock/try_wrlockvariants available. -
Includes a built-in mutex.
lock/unlockprotect the predicate.waitatomically releases the mutex and sleeps; on wakeup it re-acquires the mutex.signalwakes one waiter,broadcastwakes all. -
enterreturns true for exactly one process (the initializer); all others block untildoneis 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-blockingtry_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.