Awaiting The Future (part 2)
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
andawait
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 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:
- Construct a new CODE reference value to represent resuming the function at this point.
- 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.
- Push this code reference as an
->on_ready
callback of theFuture
that we are waiting on. - Construct a new pending
Future
instance to return to the caller. - 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:
- 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.
- Call the
->get
method on the future instance we were waiting on, in order to fetch its result as the value of theawait
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 the 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 allowasync sub
s to take signatures, as regular non-async
ones can. - An eventual plan to migrate parts of 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, 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.
Note: This post has been ported from https://tech.binary.com/ (our old tech blog). Author of this post is https://metacpan.org/author/PEVANS