class Lock::Async
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
on it, which helps against all kinds of issues resulting from data
being read and modified simultaneously from different threads.
Unlike Lock, which provides a traditional OS-backed mutual
exclusion mechanism, Lock::Async
works with the high-level concurrency
features of Raku. 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 Lock::Async
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 Raku 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 lock
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
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;
}
method protect
method protect(Lock::Async:D: &code)
This method reliably wraps code passed to &code
parameter with a
lock it is called on. It 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
code 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 different
locks, created in a bunch of threads, and thus they don't actually
protect the code we want to protect. Modifying an Array simultaneously
from different in the second example is not safe and leads to memory errors.
# Compute how many prime numbers there are in first 10 000 of them
# using 50 threads
my @primes = 0 .. 10_000;
my @results;
my @threads;
# Right: $lock is instantiated outside the portion of the
# code that will get threaded and be in need of protection,
# so all threads share the lock
my $lock = Lock::Async.new;
for ^50 -> $thread {
@threads.push: start {
$lock.protect: {
my $from = $thread * 200;
my $to = ($thread + 1) * 200;
@results.append: @primes[$from..$to].map(*.is-prime);
}
}
}
# await for all threads to finish calculation
await Promise.allof(@writers);
# say how many prime numbers we found
say "We found " ~ @results.grep(*.value).elems ~ " prime numbers";
The example below demonstrates the wrong approach: without proper locking this code will work most of the time, but occasionally will result in bogus error messages or low-level memory errors:
# !!! WRONG !!! Lock::Async is instantiated inside threaded area,
# so all the 20 threads use 20 different locks, not syncing with
# each other
for ^50 -> $thread {
@threads.push: start {
my $lock = Lock::Async.new;
$lock.protect: {
my $from = $thread * 200;
my $to = ($thread + 1) * 200;
@results.append: @primes[$from..$to].map(*.is-prime);
}
}
}
method protect-or-queue-on-recursion
method protect-or-queue-on-recursion(Lock::Async:D: &code)
When calling protect on a Lock::Async
instance that is already locked, the method is forced to block until the lock
gets unlocked. protect-or-queue-on-recursion
avoids this issue by either
behaving the same as protect if the lock is
unlocked or the lock was locked by something outside the caller chain,
returning Nil, or queueing the call to &code
and returning a Promise
if the lock had already been locked at another point in the caller chain.
my Lock::Async $lock .= new;
my Int $count = 0;
# The lock is unlocked, so the code runs instantly.
$lock.protect-or-queue-on-recursion({
$count++
});
# Here, we have caller recursion. The outer call only returns a Promise
# because the inner one does. If we try to await the inner call's Promise
# from the outer call, the two calls will block forever since the inner
# caller's Promise return value is just the outer's with a then block.
$lock.protect-or-queue-on-recursion({
$lock.protect-or-queue-on-recursion({
$count++
}).then({
$count++
})
});
# Here, the lock is locked, but not by anything else on the caller chain.
# This behaves just like calling protect would in this scenario.
for 0..^2 {
$lock.protect-or-queue-on-recursion({
$count++;
});
}
say $count; # OUTPUT: 5
method with-lock-hidden-from-recursion-check
method with-lock-hidden-from-recursion-check(&code)
Temporarily resets the Lock::Async recursion list so that it no longer includes
the lock this method is called on and runs the given &code
immediately if
the call to the method occurred in a caller chain where
protect-or-queue-on-recursion
has already been called and the lock has been placed on the recursion list.
my Lock::Async $lock .= new;
my Int $count = 0;
$lock.protect-or-queue-on-recursion({
my Int $count = 0;
# Runs instantly.
$lock.with-lock-hidden-from-recursion-check({
$count++;
});
# Runs after the outer caller's protect-or-queue-on-recursion call has
# finished running.
$lock.protect-or-queue-on-recursion({
$count++;
}).then({
say $count; # OUTPUT: 2
});
say $count; # OUTPUT: 1
});