=begin pod :kind("Type") :subkind("class") :category("basic")

=TITLE class Lock::Async

=SUBTITLE A non-blocking, non-re-entrant, mutual exclusion lock

    class Lock::Async {}

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>, which helps against all kinds of issues resulting from
data being read and modified simultaneously from different threads.

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 Raku. 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<Lock::Async> 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. Attempting 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.

While C<Lock::Async> works in terms of higher-level Raku 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

=head2 method lock

Defined as:

    method lock(Lock::Async:D: --> Promise:D)

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;
    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. If there are any outstanding 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;
    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, if 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;
    }

=head2 method protect

Defined as:

    method protect(Lock::Async:D: &code)

This method reliably wraps code passed to C<&code> parameter with a
lock it is called on.  It 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|/type/Lock::Async> 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::Async|/type/Lock::Async> is first created and assigned to
C<$lock>, which is then used I<inside> the L<Promises|/type/Promise>
code to protect the sensitive code. In the second example, a mistake is
made, the C<Lock::Async> is created right inside the
L<Promise|/type/Promise>, so the code ends up with a bunch of different
locks, created in a bunch of threads, and thus they don't actually
protect the code we want to protect. Modifying an Array I<simultaneously>
from different in the second example is not safe and leads to memory errors.

=begin code :preamble<my @writers>
# Compute how many prime numbers there are in first 10 000 of them
# using 50 threads
my @primes = 0 .. 10_000;
my @results;
my @threads;

# Right: $lock is instantiated outside the portion of the
# code that will get threaded and be in need of protection,
# so all threads share the lock
my $lock = Lock::Async.new;
for ^50 -> $thread {
    @threads.push: start {
        $lock.protect: {
            my $from = $thread * 200;
            my $to = ($thread + 1) * 200;
            @results.append: @primes[$from..$to].map(*.is-prime);
        }
    }
}

# await for all threads to finish calculation
await Promise.allof(@writers);
# say how many prime numbers we found
say "We found " ~ @results.grep(*.value).elems ~ " prime numbers";
=end code

The example below demonstrates the wrong approach: without proper locking
this code will work most of the time, but occasionally will result
in bogus error messages or low-level memory errors:

=begin code :preamble<my @threads;my @results; my @primes>
# !!! WRONG !!! Lock::Async is instantiated inside threaded area,
# so all the 20 threads use 20 different locks, not syncing with
# each other
for ^50 -> $thread {
    @threads.push: start {
        my $lock = Lock::Async.new;
        $lock.protect: {
            my $from = $thread * 200;
            my $to = ($thread + 1) * 200;
            @results.append: @primes[$from..$to].map(*.is-prime);
        }
    }
}
=end code

=head2 method protect-or-queue-on-recursion

Defined as:

    method protect-or-queue-on-recursion(Lock::Async:D: &code)

When calling L<protect|/type/Lock::Async#method_protect> on a C<Lock::Async>
instance that is already locked, the method is forced to block until the lock
gets unlocked. C<protect-or-queue-on-recursion> avoids this issue by either
behaving the same as L<protect|/type/Lock::Async#method_protect> if the lock is
unlocked or the lock was locked by something outside the caller chain,
returning C<Nil>, or queueing the call to C<&code> and returning a C<Promise>
if the lock had already been locked at another point in the caller chain.

    my Lock::Async $lock .= new;
    my Int         $count = 0;

    # The lock is unlocked, so the code runs instantly.
    $lock.protect-or-queue-on-recursion({
        $count++
    });

    # Here, we have caller recursion. The outer call only returns a Promise
    # because the inner one does. If we try to await the inner call's Promise
    # from the outer call, the two calls will block forever since the inner
    # caller's Promise return value is just the outer's with a then block.
    $lock.protect-or-queue-on-recursion({
        $lock.protect-or-queue-on-recursion({
            $count++
        }).then({
            $count++
        })
    });

    # Here, the lock is locked, but not by anything else on the caller chain.
    # This behaves just like calling protect would in this scenario.
    for 0..^2 {
        $lock.protect-or-queue-on-recursion({
            $count++;
        });
    }

    say $count; # OUTPUT: 5

=head2 method with-lock-hidden-from-recursion-check

Defined as:

    method with-lock-hidden-from-recursion-check(&code)

Temporarily resets the Lock::Async recursion list so that it no longer includes
the lock this method is called on and runs the given C<&code> immediately if
the call to the method occurred in a caller chain where
L<protect-or-queue-on-recursion|/type/Lock::Async/#method_protect-or-queue-on-recursion>
has already been called and the lock has been placed on the recursion list.

    my Lock::Async $lock .= new;
    my Int         $count = 0;

    $lock.protect-or-queue-on-recursion({
        my Int $count = 0;

        # Runs instantly.
        $lock.with-lock-hidden-from-recursion-check({
            $count++;
        });

        # Runs after the outer caller's protect-or-queue-on-recursion call has
        # finished running.
        $lock.protect-or-queue-on-recursion({
            $count++;
        }).then({
            say $count; # OUTPUT: 2
        });

        say $count; # OUTPUT: 1
    });

=end pod

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