2020/12/08

2020 Perl Advent Calendar - Day 8

<< First | < Prev | Next >

We've now had a good look at a number of situations involving asynchronous code which does one thing at a time. Back on day 3 we noted that an important use-case for asynchronous code is the ability to do multiple actions concurrently and wait for results from all of them at once. We saw a way to achieve that, by starting multiple operations at once by calling multiple asyncronous functions, then using await on each returned future in turn.

This is a sufficiently important and frequent pattern when dealing with asynchronous code and futures, that in the original advent series, day 13 introduced a constructor method, Future->needs_all, which helps this. It takes a list of futures and yields a new future, which will complete only when all of its components have completed successfully; or alternatively, will fail when any one of them has failed. Since this constructor yields a future, when using async/await syntax we can simply await on it in order to suspend until all of the components are ready.

For example, this method from a hardware chip driver needs to write to two distinct control registers in order to change the chip's configuration. It can do this most efficiently by issuing write commands to both of them individually, then waiting for them both to complete, by using the ->needs_all constructor.

use feature 'signatures';
use Future::AsyncAwait;

async sub change_config(%changes)
{
    ...
    await Future->needs_all(
        $self->write_register(REG_CTRL1, $ctrl1),
        $self->write_register(REG_CTRL3, $ctrl3),
    );
}

We can also use this structure to obtain values. When a ->needs_all future completes, it will yield a list of results by concatenating the result lists from each of its individual components. The chip driver makes use of this when reading back the configuration, by issuing two read commands for each of the control registers, and awaiting the result of both together.

use Future::AsyncAwait;

async sub read_config
{
    my ($ctrl1, $ctrl3) = await Future->needs_all(
        $self->read_register(REG_CTRL1),
        $self->read_register(REG_CTRL3),
    );
    ...
}

If a failure occurs in any of the component futures, needs_all will re-throw that failure. In effect, it acts as if we had in fact performed an await expression once on every one of the individual futures. It acts as if we had written

## A less well-written form of the above example ##
use Future::AsyncAwait;

async sub read_config
{
    my $f1 = $self->read_register(REG_CTRL1);
    my $f3 = $self->read_register(REG_CTRL3);
    
    my $ctrl1 = await $f1;
    my $ctrl3 = await $f3;
}

When waiting for more than one future like this it is preferrable to use a structure like needs_all rather than individual await expressions. Having multiple await expressions means that the containing async sub has to be resumed and suspended again each time one of them makes progress, before it finishes them all. Having a single await on the combined future only has to suspend and resume once. Results and errors are still handled just as they would be as if multiple awaits were used.

If there are more than just a few concurrent tasks to perform, there can be even better ways to express this. We will take a look at another approach tomorrow.

<< First | < Prev | Next >

No comments:

Post a Comment