class Lock
class Lock {}
A Lock
is a low-level concurrency control construct. It provides
mutual exclusion, meaning that only one thread may hold the lock at a
time. Once the lock is unlocked, another thread may then lock it.
A Lock
is typically used to protect access to one or more pieces
of state. For example, in this program:
my $x = 0;
my $l = Lock.new;
await (^10).map: {
start {
$l.protect({ $x++ });
}
}
say $x; # OUTPUT: «10»
The Lock
is used to protect operations on $x
. An increment is
not an atomic operation; without the lock, it would be possible for
two threads to both read the number 5 and then both store back the
number 6, thus losing an update. With the use of the Lock
, only
one thread may be running the increment at a time.
A Lock
is re-entrant, meaning that a thread that holds the lock can
lock it again without blocking. That thread must unlock the same number
of times before the lock can be obtained by another thread (it works by
keeping a recursion count).
It's important to understand that there is no direct connection between
a Lock
and any particular piece of data; it is up to the programmer
to ensure that the Lock
is held during all operations that involve the
data in question. The OO::Monitors
module, while not a complete solution
to this problem, does provide a way to avoid dealing with the lock explicitly
and encourage a more structured approach.
The Lock
class is backed by operating-system provided constructs, and
so a thread that is waiting to acquire a lock is, from the point of view
of the operating system, blocked.
Code using high-level Raku concurrency constructs should avoid using
Lock
. Waiting to acquire a Lock
blocks a real Thread, meaning
that the thread pool (used by numerous higher-level Raku concurrency
mechanisms) cannot use that thread in the meantime for anything else.
Any await
performed while a Lock
is held will behave in a blocking
manner; the standard non-blocking behavior of await
relies on the
code following the `await` resuming on a different Thread from the
pool, which is incompatible with the requirement that a Lock
be
unlocked by the same thread that locked it. See
Lock::Async
for an alternative mechanism that does not have this shortcoming. Other than
that, the main difference is that Lock
mainly maps to operating system
mechanisms, while Lock::Async uses Raku primitives to achieve similar
effects. If you're doing low-level stuff (native bindings) and/or actually
want to block real OS threads, use Lock
. However, if you want a
non-blocking mutual exclusion and don't need recursion and are running code
on the Raku thread pool, use Lock::Async.
By their nature, Lock
s are not composable, and it is possible to
end up with hangs should circular dependencies on locks occur. Prefer
to structure concurrent programs such that they communicate results
rather than modify shared data structures, using mechanisms like
Promise, Channel and Supply.
Methods
method protect
multi method protect(Lock:D: &code)
Obtains the lock, runs &code
, and releases the lock afterwards. Care
is taken to make sure the lock is released even if the code is left through
an exception.
Note that the Lock
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
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
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.new;
await ^20 .map: {
start {
$lock.protect: {
print "Foo";
sleep rand;
say "Bar";
}
}
}
# !!! WRONG !!! Lock is created inside threaded area!
await ^20 .map: {
start {
Lock.new.protect: {
print "Foo"; sleep rand; say "Bar";
}
}
}
method lock
method lock(Lock:D:)
Acquires the lock. If it is currently not available, waits for it.
my $l = Lock.new;
$l.lock;
Since a Lock
is implemented using OS-provided facilities, a thread
waiting for the lock will not be scheduled until the lock is available
for it. Since Lock
is re-entrant, if the current thread already holds
the lock, calling lock
will simply bump a recursion count.
While it's easy enough to use the lock
method, it's more difficult to
correctly use unlock. Instead, prefer to
use the protect method instead, which
takes care of making sure the lock
/unlock
calls always both occur.
method unlock
method unlock(Lock:D:)
Releases the lock.
my $l = Lock.new;
$l.lock;
$l.unlock;
It is important to make sure the Lock
is always released, even if
an exception is thrown. The safest way to ensure this is to use the
protect method, instead of explicitly
calling lock
and unlock
. Failing that, use a LEAVE
phaser.
my $l = Lock.new;
{
$l.lock;
LEAVE $l.unlock;
}
method condition
method condition(Lock:D: )
Returns a condition variable as a Lock::ConditionVariable object. Check this article or the Wikipedia for background on condition variables and how they relate to locks and mutexes.
my $l = Lock.new;
$l.condition;
You should use a condition over a lock when you want an interaction with it that is a bit more complex than simply acquiring or releasing the lock.
constant ITEMS = 100;
my $lock = Lock.new;
my $cond = $lock.condition;
my $todo = 0;
my $done = 0;
my @in = 1..ITEMS;
my @out = 0 xx ITEMS;
loop ( my $i = 0; $i < @in; $i++ ) {
my $in := @in[$i];
my $out := @out[$i];
Thread.start( {
my $partial = $in² +1;
if $partial.is-prime {
$out = $partial but "Prime";
} else {
$out = $partial;
}
$lock.protect( {
$done++;
$cond.signal if $done == $todo;
} );
} );
$todo++;
}
$lock.protect( {
$cond.wait({ $done == $todo } );
});
say @out.map: { $_.^roles > 2 ?? $_.Num ~ "*" !! $_ };
# OUTPUT: «2* 5* 10 17* 26 37* 50 65 82 101* … »
In this case, we use the condition variable $cond
to wait until all
numbers have been generated and checked and also to .signal
to another
thread to wake up when the particular thread is done.