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 = 0;my = Lock.new;await (^10).map: say ; # 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 §
Defined as:
multi method protect(Lock: )
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.new;await ^20 .map: # !!! WRONG !!! Lock is created inside threaded area! await ^20 .map:
method lock §
Defined as:
method lock(Lock:)
Acquires the lock. If it is currently not available, waits for it.
my = Lock.new;.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 §
Defined as:
method unlock(Lock:)
Releases the lock.
my = Lock.new;.lock;.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 = Lock.new;
method condition §
Defined as:
method condition(Lock: )
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 = Lock.new;.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.new;my = .condition;my = 0;my = 0;my = 1..ITEMS;my = 0 xx ITEMS; loop ( my = 0; < ; ++ ) .protect( ); say .map: ;# 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.