2020/12/10

2020 Perl Advent Calendar - Day 10

<< First | < Prev | Next >

In the past couple of days we've seen some techniques for performing multiple concurrent calls to asynchronous functions and waiting for them all to succeed. Sometimes there are situations when we want to make a number of calls and wait for any one of them to complete and take the first result that arrives, rather than waiting for them all.

The original blog series post on day 21 introduced Future->wait_any which also a list of futures and returns one to represent their combination, in a pattern similar to needs_all. Where needs_all needed all of the components to complete, this one will yield the result of whichever is first to finish - either in success or failure. In effect it creates a race between them.

This is most often seen in order to create timeouts around asynchronous functions. As with the needs_any constructor, it is usually seen directly in an await expression. Its arguments, the component futures it waits on, are often the result of directly calling asynchronous functions. Often there are just two arguments - one future to do some "useful" work and yield a successful result, the other to wait until the timeout and then yield a failure. Whichever completes first will be the result of the wait_any and hence what await will see.

For example, this method from a communication protocol sends a byte over a filehandle, then waits for either a single-byte read back from the filehandle, or a timeout.

use Future::AsyncAwait;
use Future::IO;

async sub _break
{
    my $self = shift;
    my $fh = $self->{fh};
    
    $fh->print("\x00");

    await Future->wait_any(
        Future::IO->sysread($fh, 1),
        Future::IO->sleep(0.05)->then_fail("Timed out"),
    );
}

We will introduce the Future::IO module more fully in a later post. For now, consider that it provides some methods named after core IO functions which return futures and allow the operations to take place asynchronously. In this example the sysread future will complete if there is another byte available on the filehandle, and the sleep future will complete after the given delay. The ->then_fail method is a small shortcut on a future instance, for turning a success into a failure. The result here is that if a new byte is soon available to read from the filehandle then it is returned by the first component future, though if none arrives after 50msec the second future yields a failure, which causes the await to throw an exception up to its own caller.

You may be wondering what happens to the sleep call if the byte is read successfully, or to the sysread if the timeout happens. The answer here is that as well as completing with success or failure, pending future instances can also be cancelled. As soon as one of the component futures that wait_any is waiting for is completed, it will cancel all of the other ones. It is up to the creator of the component futures to determine what should happen at this point. The original blog post series at day 19 introduced the on_cancel method, which the implementation can use to set this behaviour.

When using async/await syntax this is mostly handled automatically. While an async sub is suspended waiting for a future to complete, if its own return-value future is cancelled then this cancellation is propagated down to the future it was waiting on.

Cancellation is mostly a feature of interest to the lowest level implementation of asynchronous systems using futures, and doesn't often need special handling in intermediate layers of logic. For situations where the logic does however need to handle it, you can use a CANCEL block. At time of writing this syntax is still considered experimental, but it is designed to feel similar to my other still-experimental idea of adding FINALLY blocks to core Perl syntax.

use Future::AsyncAwait ':experimental(cancel)';

async sub f
{
   CANCEL { warn "This task was cancelled"; }
 
   await ...
}

It turns out in actual practice to be quite rare to need this ability - I had hoped to be able to paste a real example from some real code, but currently there isn't any on CPAN which actually makes use of this. Hopefully there will eventually be enough actual uses of the syntax to be able to judge the experiment, and see whether it should become stable, or still needs work.

<< First | < Prev | Next >

No comments:

Post a Comment