A low-level, re-entrant, mutual exclusion lock
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.
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␤»
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.
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.
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.
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,
Locks 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.
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 protectionmy = Lock.new;await ^20 .map:# !!! WRONG !!! Lock is created inside threaded area!await ^20 .map:
Acquires the lock. If it is currently not available, waits for it.
my = Lock.new;.lock;
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
unlock calls always both occur.
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
unlock. Failing that, use a
my = Lock.new;
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.