Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Improve Lock and Lock::Async documentation
* Better explain what Lock::Async is
* Provide a more complete and accurate introduction to Lock, moving
  general information of the mechanism out of the description of the
  lock method
* Improve the lock, unlock, and protect descriptions for both
* Indicate how to use unlock more safely (with LEAVE)
* Provide comparisons of these two mechanisms and link between them,
  to help folks pick the correct one

Resolves #2784.
  • Loading branch information
jnthn committed May 12, 2019
1 parent a7c52cd commit 518d24b
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 51 deletions.
101 changes: 70 additions & 31 deletions doc/Type/Lock.pod6
Expand Up @@ -2,13 +2,16 @@
=TITLE class Lock
=SUBTITLE Low-level thread locking primitive
=SUBTITLE A low-level, re-entrant, mutual exclusion lock
class Lock {}
A Lock is a low-level constructor for ensuring that only one thread
works with a certain object at a given time, or runs a piece of code
(called the I<critical section>).
A C<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 C<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;
Expand All @@ -19,13 +22,45 @@ works with a certain object at a given time, or runs a piece of code
}
say $x; # OUTPUT: «10␤»
Locks are re-entrant, that is, a thread that holds the lock can lock it
again without blocking.
High-level Perl 6 code should avoid the direct usage of locks, because
they are not composable. Instead, high-level constructs such as
L<Promise|/type/Promise>, L<Channel|/type/Channel> and L<Supply|/type/Supply> should
be used whenever possible.
The C<Lock> is used to protect operations on C<$x>. An increment is
not an atomic opreation; 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 C<Lock>, only
one thread may be running the increment at a time.
A C<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 obtaiend by another thread (it works by
keeping a recursion count).
It's important to understand that there is no direct connection between
a C<Lock> and any particular piece of data; it is up to the programmer
to ensure that the C<Lock> is held during all operations that involve the
data in question. The C<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 C<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 Perl 6 concurrency constructs should avoid using
C<Lock>. Waiting to acquire a C<Lock> blocks a real C<Thread>, meaning
that the thread pool (used by numerous higher-level Perl 6 concurrency
mechanisms) cannot use that thread in the meantime for anything else.
Any C<await> performed while a C<Lock> is held will behave in a blocking
manner; the standard non-blocking behavior of C<await> relies on the
code following the `await` resuming on a different C<Thread> from the
pool, which is incompatible with the requirement that a C<Lock> be
unlocked by the same thread that locked it. See L<Lock::Async|/type/Lock/Async>
for an alternative mechanism that does not have this shortcoming.
By their nature, C<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
L<Promise|/type/Promise>, L<Channel|/type/Channel> and L<Supply|/type/Supply>.
=head1 Methods
Expand All @@ -35,13 +70,15 @@ Defined as:
method protect(Lock:D: &code)
Runs C<&code> and makes sure it is only run in one thread at once.
Obtains the lock, runs C<&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 L<Lock|/type/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, L<Lock|/type/Lock> is first created and assigned to C<$lock>,
which is then used I<inside> the L<Promises|/type/Promise> to protect
the sensitive code. In the second example, a mistake is made, the
the sensitive code. In the second example, a mistake is made: the
C<Lock> is created right inside the L<Promise|/type/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.
Expand Down Expand Up @@ -79,22 +116,15 @@ Acquires the lock. If it is currently not available, waits for it.
my $l = Lock.new;
$l.lock;
This construct provides a low-level, OS-backed lock (on most platforms,
it is a pthreads mutex, for example). The lock is reentrant, meaning
that if you acquire it while already holding it then it just bumps a
recursion count. Trying to acquire the lock when something else is
holding it will really block a thread. If you block a thread pool thread
with this construct, it won't be able to work on anything else in the
meantime. Further, an await while holding a lock will not function in a
non-blocking manner (as a threadpool await normally would in 6.d).
For that reason, it's better to use
L<C<protect>|/type/Lock#method_protect> instead of an explicit
lock/unlock. That makes sure that the lock is always released. If you
forget to unlock then you'll "lose" the lock and nothing will ever be
able to acquire it again, probably resulting in the program deadlocking.
If you really want to do the unlock yourself, the safest way is in a
C<LEAVE> phaser.
Since a C<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 C<Lock> is re-entrant, if the current thread already holds
the lock, calling C<lock> will simply bump a recursion count.
While it's easy enough to use the C<lock> method, it's more difficult to
correctly use L<C<unlock>|/type/Lock#method_unlock>. Instead, prefer to
use the L<C<protect>|/type/Lock#method_protect> method instead, which
takes care of making sure the C<lock>/C<unlock> calls always both occur.
=head2 method unlock
Expand All @@ -108,9 +138,18 @@ Releases the lock.
$l.lock;
$l.unlock;
Please see the description of L<C<.lock>|/type/Lock#method_lock> above.
It is important to make sure the C<Lock> is always released, even if
an exception is thrown. The safest way to ensure this is to use the
L<C<protect>|/type/Lock#method_protect> method, instead of explicitly
calling C<lock> and C<unlock>. Failing that, use a C<LEAVE> phaser.
my $l = Lock.new;
{
$l.lock;
LEAVE $l.unlock;
}
=head2 method condition
Defined as:
Expand All @@ -134,4 +173,4 @@ L<https://en.wikipedia.org/wiki/Monitor_%28synchronization%29> for background.
=end pod

# vim: expandtab softtabstop=4 shiftwidth=4 ft=perl6
# vim: expandtab softtabstop=4 shiftwidth=4 ft=perl6
77 changes: 57 additions & 20 deletions doc/Type/Lock/Async.pod6
Expand Up @@ -2,23 +2,38 @@
=TITLE class Lock::Async
=SUBTITLE Low-level non-blocking non-re-entrant mutual exclusion lock
=SUBTITLE A non-blocking, non-re-entrant, mutual exclusion lock
class Lock::Async {}
An asynchronous lock provides a non-blocking non-re-entrant mechanism for
mutual exclusion. The lock method returns a Promise, which will already be
kept if nothing was holding the lock already, so execution can proceed
immediately. For performance reasons, in this case it returns a singleton
Promise instance. Otherwise, a Promise in planned state will be returned,
and kept once the lock has been unlocked by its current holder. The lock
and unlock do not need to take place on the same thread; that's why it's not
A C<Lock::Async> instance provides a mutual exclusion mechanism: when the
lock is held, any other code wishing to C<lock> must wait until the holder
calls C<unlock>.
Unlike L<Lock|/type/Lock>, which provides a traditional OS-backed mutual
exclusion mechanism, C<Lock::Async> works with the high-level concurrency
features of Perl 6. The C<lock> method returns a C<Promise>, which will
be kept when the lock is available. This C<Promise> can be used with
non-blocking C<await>. This means that a thread from the thread pool need
not be consumed while waiting for the C<Async::Lock> to be available,
and the code trying to obtain the lock will be resumed once it is available.
The result is that it's quite possible to have many thousands of outstanding
C<Lock::Async> lock requests, but just a small number of threads in the
pool. Attemtping that with a traditional L<Lock|/type/Lock> would not go so
well!
There is no requirement that a C<Lock::Async> is locked and unlocked by the
same physical thread, meaning it is possible to do a non-blocking C<await>
while holding the lock. The flip side of this is C<Lock::Async> is not
re-entrant.
High-level Perl 6 code should avoid the direct usage of locks, because they
are not composable. Instead, high-level constructs such as
L<Channel|/type/Channel> and L<Supply|/type/Supply> should be used
whenever possible.
While C<Lock::Async> works in terms of higher-level Perl 6 concurrency
mechanisms, it should be considered a building block. Indeed, it lies at
the heart of the C<Supply> concurrency model. Prefer to structure programs
so that they communicate results rather than mutate shared data structures,
using mechanisms like L<Promise|/type/Promise>, L<Channel|/type/Channel>
and L<Supply|/type/Supply>.
=head1 Methods
Expand All @@ -28,7 +43,9 @@ Defined as:
method protect(Lock::Async:D: &code)
Runs C<&code> and makes sure it is only run in one thread at once.
Calls C<lock>, does an C<await> to wait for the lock to be available,
and reliably calls C<unlock> afterwards, even if the code throws an
exception.
Note that the L<Lock::Async> itself needs to be created outside the portion
of the code that gets threaded and it needs to protect. In the first
Expand Down Expand Up @@ -68,25 +85,45 @@ Defined as:
method lock(Lock::Async:D: --> Promise:D)
Acquires the lock, does B<not> wait to acquire the lock. Returns a Promise
that will be kept whenever the lock is acquired.
Returns a L<Promise|/type/Promise> that will be kept when the lock is
available. In the case that the lock is already available, an already
kept C<Promise> will be returned. Use C<await> to wait for the lock to
be available in a non-blocking manner.
my $l = Lock::Async.new;
$l.lock;
await $l.lock;
Prefer to use L<protect|/type/Lock/Async#method_protect> instead of
explicit calls to C<lock> and C<unlock>.
=head2 method unlock
Defined as:
method unlock(Lock::Async:D: --> Nil)
Releases the lock, blocking until all Promised of all holders of the lock have
been kept.
Releases the lock. If there are any oustanding C<lock> C<Promise>s,
the one at the head of the queue will then be kept, and potentially
code scheduled on the thread pool (so the cost of calling C<unlock>
is limited to the work needed to schedule another piece of code that
wants to obtain the lock, but not to execute that code).
my $l = Lock::Async.new;
$l.lock;
await $l.lock;
$l.unlock;
Prefer to use L<protect|/type/Lock/Async#method_protect> instead of
explicit calls to C<lock> and C<unlock>. However, it wishing to use
the methods separately, it is wise to use a C<LEAVE> block to ensure
that C<unlock> is reliably called. Failing to C<unlock> will mean that
nobody can ever C<lock> this particular C<Lock::Async> instance again.
my $l = Lock::Async.new;
{
await $l.lock;
LEAVE $l.unlock;
}
=end pod

# vim: expandtab softtabstop=4 shiftwidth=4 ft=perl6
# vim: expandtab softtabstop=4 shiftwidth=4 ft=perl6

0 comments on commit 518d24b

Please sign in to comment.