A further look at Future::AsyncAwait, where it will go next and how we should be using it.

In the previous part, I introduced the Future::AsyncAwait module, which adds the async and await pair of keywords. These help us to write many forms of code that use futures for asynchronous behaviour.

Inner Details of Implementation

As a brief overview, the internals of the Future::AsyncAwait module fall into three main parts:

  • Suspend and resume of a running CV (Perl function)
  • Keyword parser to recognise the async and await keywords
  • Glue logic to connect these two parts with the Future objects

Of these three parts, the first one is by far the largest, most complex, and where a lot of the ongoing bug-fixing and feature enhancement is happening. The keyword parsing and Future glue logic form a relatively small contribution to the overall code size, and have proved to be quite stable over the past year or so.

The main reason that the suspend and resume functionality is so large and complex is that there is a lot of state in a lot of places of the Perl interpreter which needs to be saved when a running function is to be suspended, and then put back into the right place when it resumes. A significant portion of the time taken to develop it so far has been research into the inner workings of the Perl interpreter to find all these details, and work out how to manage it.

When a running function needs to be suspended at the await expression, the following steps are taken:

  1. Construct a new CODE reference value to represent resuming the function at this point.
  2. Capture and save as much of the running state of the function as we can - the nested scope blocks, the values of lexical variables, temporary values on the stack, etc... This state is attached to the new CODE reference.
  3. Push this code reference as an ->on_ready callback of the Future that we are waiting on.
  4. Construct a new pending Future instance to return to the caller.
  5. Make the async sub call return with this instance as its result.

At this point, the running function has been suspended, with its state captured by a CODE reference stored in the future instance it is waiting for. Once that future becomes ready, it invokes its ->on_ready callbacks in the usual way, among them being the CODE reference we stored there. This then takes the following steps:

  1. Restore the state of the running function from the values captured by the suspend process. This has to restore the nested scope blocks, lexical variables, etc... in the reverse order from how they were captured.
  2. Call the ->get method on the future instance we were waiting on, in order to fetch its result as the value of the await expression.

At this point, the await expression is now done and it can simply resume the now-restored function as normal.

Known Bugs

As already mentioned, the module is not yet fully production-ready as it is known to have a few issues, and likely there may be more lurking around as yet unknown. As an outline of the current state of stability, and to suggest the size and criticality of the currently-known issues, here are a few of the main ones:

Labelled loop controls do not work

(https://rt.cpan.org/Public/Bug/Display.html?id=128205)

Simple foreach and while loops work fine, but using labels with loop control keywords (next, last, redo) currently result in a failure. For example:

ITEM: foreach my $item ( @items ) {
   my $result = await get_item($item);
   defined $result or next ITEM;
   push @results, $result;
}

The label search here will fail, with

Label not found for `next ITEM` at ...

The current workaround for this issue is simply not to make use of labelled loop controls across await boundaries.

Edit: This bug was fixed in version 0.22.

Complex expressions in foreach lose values

(https://rt.cpan.org/Public/Bug/Display.html?id=128619)

I haven't been able to isolate a minimal test case yet for this one, but in essence the bug is that given some code which performs

foreach my $value ( (1) x ($len - 1), (0) ) {
    await ...
}

the final 0 value gets lost. The loop executes for $len - 1 times with $value set to 1, but misses the final 0 case.

The current workaround for this issue is to calculate the full set of values for the loop to iterate on into an array variable, and then foreach over the array:

my @values = ( (1) x ($len - 1), (0) );
foreach my $value ( @values ) {
    await ...
}

While an easy workaround, the presence of this bug is nonetheless a little worrying, because it demonstrates the possibility for a silent failure. The code doesn't cause an error message or a crash, it simply produces the wrong result without any warning or other indication that anything went wrong. It is, at time of writing, the only bug of this kind known. Every other bug produces an error message, most likely a crash, either at compile or runtime.

Edit: This bug was fixed in version 0.23.

Next Directions

Aside from fixing the known bugs, two of which are outlined above, there are a few missing features or other details that should be addressed at some point soon.

Core perl integration

Currently, the module operates entirely as a third-party CPAN module, without specific support from the Perl core. While the perl5-porters ("p5p") are aware of and generally encourage this work to continue, there is no specific integration at the code level to directly assist. There are two particular details that I would like to see:

  • Better core support for parsing and building the optree fragment relating to the signature part of a sub definition. Currently, async sub definitions cannot make use of function signatures, because the parser is not sufficiently fine-grained to allow it. An interface in core Perl to better support this would allow async subs to take signatures, as regular non-async ones can.
  • An eventual plan to migrate parts of the suspend and resume logic out of this module and into core. Or at least, some way to try to make it more future-proof. Currently the implementation is very version-dependent and has to inspect and operate on lots of various inner parts of the Perl interpreter. If core Perl could offer a way to suspend and resume a running CV, it would make Future::AsyncAwait a lot simpler and more stable across versions, and would also pave the way for other CPAN modules to provide other syntax or semantics based around this concept, such as coroutines or generators.

local and await

Currently, the suspend logic will get upset about any local variable modifications that are in scope at the time it has to suspend the function; for instance

async sub x {
    my $self = shift;
    local $self->{debug} = 1;
    await $self->do_work();
    # is $self->{debug} restored to 1 here?
}

This is more than just a limit of the implementation, however as it extends to fundamental questions about what the semantic meaning of such code should be. It is hard to draw parallels from any of the other language the async/await syntax was inspired by, because none of these have a construct similar to Perl's local.

Recommendations For Use

Earlier, I stated that Future::AsyncAwait is not fully production-ready yet, on account of a few remaining bugs combined with its general lack of production testing at volume. While it probably shouldn't be used in any business-critical areas at the moment, it can certainly help in many other areas.

Tom Molesworth writes that it should be avoided in the critical money-making end of production code, but considered on a case-by-case basis in new features, and definitely considered for unit tests. He adds:

Any code which uses it should have good test coverage, and also go through load-testing. [...] Simple, readable code is going to be a benefit that may outweigh the potential risks of using newer, less-well-tested modules such as this one.

This advice is similar to my own personal uses of the module, which are currently limited to a small selection of my CPAN modules that relate to various exotic pieces of hardware.

I am finding that the overall neatness and expressiveness of using async/await expressions is easy justification against the potential for issues in these areas. As bugs are fixed and the module is found to be increasingly stable and reliable, the boundary can be further pushed back and the module introduced to more places.