class Lock

A low-level, re-entrant, mutual exclusion 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.

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.

Methods

method protect

Defined as:

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 randsay "Bar";
        }
    }
}

method lock

Defined as:

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

Defined as:

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

Defined as:

my class ConditionVariable {
    method wait();
    method signal();
    method signal_all();
}
 
method condition(Lock:D: --> ConditionVariable:D)

Returns a condition variable. Compare https://web.stanford.edu/~ouster/cgi-bin/cs140-spring14/lecture.php?topic=locks or https://en.wikipedia.org/wiki/Monitor_%28synchronization%29 for background.

my $l = Lock.new;
$l.condition;

Type Graph

Type relations for Lock
perl6-type-graph Lock Lock Any Any Lock->Any Mu Mu Any->Mu

Expand above chart