2020/12/03

2020 Perl Advent Calendar - Day 3

<< First | < Prev | Next >

Yesterday, we saw await being used at the program toplevel, where its behaviour is simply to wait for a future to finish, and we saw some definitions of functions declared as async sub which return futures. One of the defining features of the async/await syntax is the way in which await expressions can be used inside async functions. Used in such a way, the asynchronous function will suspend itself until the result is available on the future it is waiting for, and then the function will be resumed. While suspended, the future returned by it remains pending, and the caller is free to perform other tasks while it continues. This lets us build a larger piece of asynchronous behaviour out of smaller pieces.

For example, we can use this to create a function that fetches an HTML page, then uses Mojo::DOM to parse and extract the page title from it.

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

async sub get_page_title($url)
{
    my $response = await GET($url);

    my $dom = Mojo::DOM->new($response->body);
    
    return $dom->at("title")->text;
}

As with yesterday's examples, we can now call this function in a toplevel await expression to wait for its result. Since this is all about concurrency and the efficiency gains by performing multiple tasks at once, lets now attempt to fetch the titles of three different pages. Similar to yesterday, we can do this by starting all three requests and returning three futures, then await each of them. (In a later post we will see a better way to perform logic like this, but for this small example of just three pages, this will do fine.)

use Future::AsyncAwait;

my @futures = (
    get_page_title("http://my-site-here.com/page1.html"),
    get_page_title("http://my-site-here.com/page2.html"),
    get_page_title("http://my-site-here.com/page3.html"),
);

say "The titles of the pages are:";
say await $_ for @futures;

In this way we can begin to build asynchronous functions by calling smaller ones in the same way that we would write synchronous code. This starts to give us a hint on how the GET function itself may be implemented in terms of some helpers for the various stages of an HTTP client request/response cycle:

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

async sub connect_tcp($host, $port) { ... }

async sub send_http_request($sock, $request) { ... }

async sub recieve_http_response($sock) { ... }

async sub GET($url)
{
    my $sock = await connect_tcp($url->host, $url->port);
    
    my $request = HTTP::Request->new(GET => $url->path_query,
        [Host => $url->authority],
    );
    
    await send_http_request($sock, $request);
    
    my $response = await receive_http_response($sock);
    
    return $response;
}

Of course, this is just a toy example. Any real piece of robust and reliable code would have to deal with such subjects as error handling, conditional branching, repetitions and so on, as well as storing persistent state between invocations. Fortunately, these are all things for which async/await syntax is well-suited, so lets explore them next.

<< First | < Prev | Next >

No comments:

Post a Comment