The Lock Class
When writing concurrent code in Raku, we want to avoid sharing data between
tasks. This is because code that shares no data is automatically safe and
doesn’t have to worry about interdependencies with other code. So, when you can,
you should do your work through Supply
, Promise
, and Channel
objects that
are then synchronized together by a central thread. That way, all the state
changes are safe.
This is not always practical, though. Sometimes it really is more efficient to share data that can change between tasks running on separate threads. However, such sharing is inherently unsafe.
For example, here’s a very simple multi-threaded application in Raku that is not thread safe and will lead to incorrect results:
my $x = 0;
my @p = (
start for ^100 { $x++; sleep rand/10; },
start for ^100 { $x++; sleep rand/10; },
);
await Promise.allof(@p);
say $x;
We might expect the value of $x
to be 200
, but it is very unlikely it will be
200
. It will almost certainly be lower. This is because the simple $x++
operation needs to:
- Read the value of
$x
. - Add one to the value read from
$x
. - Store the calculated value back int
$x
.
If it happens that the first task performs step 1 and 2 and then second task performs steps 1 through 3 before the first task completes step 3, at least one of the increment operations will be lost. Over the course of 100 iterations in each task, I would expect 5 or 10 writes to be lost and each run is likely to give a slightly different answer. This is what it means to be unsafe when it comes to concurrency.
For this particular program, the three steps above constitute a critical
section. A critical section is a piece of code that must be performed
sequentially if it is to work at all. In the Raku code itself, the critical
section is just the $x++
statement in each loop.
One mechanism for ensuring a critical section in your code is handled in a
thread safe way is to use a mutual exclusion lock. Raku provides the
Lock
class which can be used for this
purpose.
When you create a Lock
object, you can .lock
or .unlock
the lock in
your code. Calling the .lock
method will block your code from running further
if any other code has called .lock
on the same object without calling .unlock
.
As soon as the thread holding the lock calls .unlock
, another thread waiting
for the lock to release will be allowed to continue.
In our example above, we might modify it as follows to make it thread safe:
my $x = 0;
my $lock = Lock.new;
my @p = (
start for ^100 { $lock.lock; $x++; $lock.unlock; sleep rand/10; },
start for ^100 { $lock.protect: { $x++ }; sleep rand/10; }
);
await Promise.allof(@p);
say $x;
I didn’t mention .protect
, but it does the same thing as calling .lock
,
running the given block, and then running .unlock
. It has the advantage though
of doing a thorough job of making the .unlock
call happens if something goes
wrong inside the block. In the first loop above where we use .lock
and
.unlock
, it is possible for an exception thrown to cause the lock to remain
permanently locked. Using .protect
automatically avoids this risk, so it’s the
preferred way to use Lock
.
Before I conclude, I want to mention a couple negatives to locks. First, locks do not perform very well. They are simple to implement and straightforward to use, but mutual exclusion can come at a high cost to performance. You probably want to make sure your locks are used sparingly and used only to protect the critical sections.
The other major drawback is that when locks are used, you can get thread safety,
but may add the risk for deadlock. I mentioned one deadlock risk already: a
task causing an error that prevents a lock from being unlocked. That lock is now
dead and no task can take it. When multiple locks are involved, the risk for
deadlock can be much more subtle and very difficult to find. Unlike Supply
or
Promise
, a Lock
is not safely composeable. This means two libraries using
locks to secure themselves might deadlock when used together if you are not
careful.
Despite their drawbacks, locks are useful things for making code thread safe. Later in this Advent Calendar we will use locks to demonstrate how to create thread safe data structures.
Cheers.