Sterling has too many projects Blogging about Raku programming, microcontrollers & electronics, 3D printing, and whatever else...

The Lock Class

| 755 words | 4 minutes | raku advent-2019
A padlock

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:

  1. Read the value of $x.
  2. Add one to the value read from $x.
  3. 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.

The content of this site is licensed under Attribution 4.0 International (CC BY 4.0).