class Lock::Async

A non-blocking, non-re-entrant, mutual exclusion lock

class Lock::Async {}

A Lock::Async instance provides a mutual exclusion mechanism: when the lock is held, any other code wishing to lock must wait until the holder calls unlock.

Unlike Lock, which provides a traditional OS-backed mutual exclusion mechanism, Lock::Async works with the high-level concurrency features of Perl 6. The lock method returns a Promise, which will be kept when the lock is available. This Promise can be used with non-blocking await. This means that a thread from the thread pool need not be consumed while waiting for the Async::Lock to be available, and the code trying to obtain the lock will be resumed once it is available.

The result is that it's quite possible to have many thousands of outstanding Lock::Async lock requests, but just a small number of threads in the pool. Attempting that with a traditional Lock would not go so well!

There is no requirement that a Lock::Async is locked and unlocked by the same physical thread, meaning it is possible to do a non-blocking await while holding the lock. The flip side of this is Lock::Async is not re-entrant.

While Lock::Async works in terms of higher-level Perl 6 concurrency mechanisms, it should be considered a building block. Indeed, it lies at the heart of the Supply concurrency model. Prefer to structure programs so that they communicate results rather than mutate shared data structures, using mechanisms like Promise, Channel and Supply.

Methods

method protect

Defined as:

method protect(Lock::Async:D: &code)

Calls lock, does an await to wait for the lock to be available, and reliably calls unlock afterwards, even if the code throws an exception.

Note that the Lock::Async itself needs to be created outside the portion of the code that gets threaded and it needs to protect. In the first example below, Lock::Async is first created and assigned to $lock, which is then used inside the Promises to protect the sensitive code. In the second example, a mistake is made, the Lock::Async is created right inside the Promise, so the code ends up with a bunch of separate locks, created in a bunch of threads, and thus they don't actually protect the code we want to protect.

# Right: $lock is instantiated outside the portion of the 
# code that will get threaded and be in need of protection 
my $lock = Lock::Async.new;
await ^20 .map: {
    start {
        $lock.protect: {
            print "Foo";
            sleep rand;
            say "Bar";
        }
    }
}
 
# !!! WRONG !!! Lock::Async is instantiated inside threaded area! 
await ^20 .map: {
    start {
        my $lock = Lock::Async.new;
        $lock.protect: {
            print "Foo"sleep randsay "Bar";
        }
    }
}

method lock

Defined as:

method lock(Lock::Async:D: --> Promise:D)

Returns a Promise that will be kept when the lock is available. In the case that the lock is already available, an already kept Promise will be returned. Use await to wait for the lock to be available in a non-blocking manner.

my $l = Lock::Async.new;
await $l.lock;

Prefer to use protect instead of explicit calls to lock and unlock.

method unlock

Defined as:

method unlock(Lock::Async:D: --> Nil)

Releases the lock. If there are any outstanding lock Promises, the one at the head of the queue will then be kept, and potentially code scheduled on the thread pool (so the cost of calling unlock is limited to the work needed to schedule another piece of code that wants to obtain the lock, but not to execute that code).

my $l = Lock::Async.new;
await $l.lock;
$l.unlock;

Prefer to use protect instead of explicit calls to lock and unlock. However, if wishing to use the methods separately, it is wise to use a LEAVE block to ensure that unlock is reliably called. Failing to unlock will mean that nobody can ever lock this particular Lock::Async instance again.

my $l = Lock::Async.new;
{
    await $l.lock;
    LEAVE $l.unlock;
}