NAME

Acme::Parataxis::Sync::Mutex - Non-Reentrant Mutual Exclusion Lock with True Owner Tracking

SYNOPSIS

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

my $lock  = Acme::Parataxis::Sync::Mutex->new;
my $count = 0;

async {
    my @workers = map {
        fiber {
            my $guard = $lock->guard;    # acquired; released on scope exit
            $count++;
        };
    } 1 .. 10;

    $_->await for @workers;
    say "Final count: $count";           # 10
};

DESCRIPTION

Acme::Parataxis::Sync::Mutex is a cooperative mutual exclusion lock designed for fibers. It tracks the identity of the holding fiber explicitly: only the fiber that acquired the lock may release it.

Key guarantees:

  • Non-reentrant: Attempting to lock a mutex you already hold croaks with Mutex is not reentrant: this fiber already holds the lock. This makes self-deadlocks loud, immediate failures rather than silent hangs.

  • Fair, FIFO hand-off: When a fiber unlocks the mutex, the lock is handed directly to the longest-waiting fiber in the queue. The ownership transition is atomic: owner is assigned to the waking fiber before it is re-enqueued onto the scheduler run queue, preventing unqueued fibers from barging in and stealing the lock.

  • Cancellable waits: Waiting fibers park cooperatively and can be interrupted by with_timeout or cancellation tokens. An interrupted waiter removes itself from the queue cleanly.

METHODS

new( )

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

Constructs a new, unlocked mutex. Takes no arguments.

lock( )

$lock->lock;

Acquires the lock. If the mutex is already held by another fiber, the calling fiber parks until ownership is handed over. Returns true.

  • Croaks if called outside a scheduled fiber.

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

try_lock( )

if ( $lock->try_lock ) {
    # acquired lock without parking
    $lock->unlock;
} else {
    # held by another fiber
}

Attempts to acquire the lock without parking:

  • If free: claims ownership and returns true.

  • If held by another fiber: returns false immediately.

  • If already held by the calling fiber: croaks with the re-entrancy error.

unlock( )

$lock->unlock;

Releases the lock and hands ownership to the next live fiber queued in waiters.

  • Croaks if the mutex is not currently locked.

  • Croaks if called from a fiber other than the current owner.

Returns true.

guard( )

{
    my $guard = $lock->guard;
    # critical section
}

Acquires the lock via lock() and returns an opaque Acme::Parataxis::Sync::Mutex::Guard object. When the guard is destroyed (via normal block exit, return, or an exception), it invokes unlock() automatically.

owner( )

Returns the numeric fiber ID of the fiber currently holding the lock, or undef if the lock is free.

waiters( )

Returns the number of fibers currently parked waiting to acquire the lock. Note that when unlock() is called, ownership is transferred and the waiter is dequeued immediately, so waiters decrements before the waking fiber actually resumes execution.

Recipes

Scoped critical sections

Always prefer guards over manual lock()/unlock() pairs to ensure releases occur even during exceptions:

sub update_shared_state ($mutex, $data) {
    my $guard = $mutex->guard;
    validate($data);    # if this dies, $guard->DESTROY safely unlocks!
    apply($data);
}

Non-blocking skip

Use try_lock() to perform work only when no contention exists:

fiber {
    if ( $lock->try_lock ) {
        my $guard = Acme::Parataxis::Sync::Mutex::Guard->new( mutex => $lock );
        do_background_sync();
    } else {
        say "Lock busy; skipping this sync cycle";
    }
};

Behavior Notes

  • Locks outlive fibers

    If a fiber acquires a lock and terminates without unlocking (and without a guard), the mutex remains locked permanently. Any subsequent fiber attempting to acquire the lock will park forever, eventually triggering a deadlock. Always capture guards in a lexical variable within the acquiring fiber.

  • Guards cannot cross fibers

    Guards verify ownership upon destruction. If you create a guard in fiber A and pass the guard object to fiber B, fiber B's destructor will croak with Mutex release from a fiber that is not the owner, stranding the lock.

  • Atomic hand-off safety

    If a release occurs and ownership is transferred to a parked fiber at the exact instant that fiber's with_timeout deadline fires, the cleanup logic detects the collision, resets the owner, and passes the lock on to the next live waiter in the queue.

SEE ALSO

Acme::Parataxis::Sync, Acme::Parataxis::Sync::RwLock, Acme::Parataxis::Semaphore

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.