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