Asynchronous programming

Rune has first class support for Rust-like asynchronous programming. In this section we'll be briefly covering what asynchronous programming is, and how it applies to Rune as a dynamic programming language.

What is it?

Asynchronous code allows us to run multiple tasks concurrently, and work with the result of those tasks.

A typical example would be if we want to perform multiple HTTP requests at once:

let a = http::get("https://google.com");
let b = http::get("https://amazon.com");

loop {
    let res = select {
        res = a => res?,
        res = b => res?,
    };

    match res {
        () => break,
        result => {
            println!("{}", result.status());
        }
    }
}
$> cargo run -- run scripts/book/async/async_http.rn
200 OK
200 OK

In the above code we send two requests concurrently. They are both processed at the same time and we collect the result.

select blocks

A fundamental construct of async programming in Rune is the select block. It enables us to wait on a set of futures at the same time.

A simple example of this is if we were to implement a simple request with a timeout:

struct Timeout;

async fn request(timeout) {
    let request = http::get(`http://httpstat.us/200?sleep=${timeout}`);
    let timeout = time::sleep(time::Duration::from_secs(2));

    let result = select {
        _ = timeout => Err(Timeout),
        res = request => res,
    }?;

    println!("{}", result.status());
    Ok(())
}

if let Err(Timeout) = request(1000).await {
    println!("Request timed out!");
}

if let Err(Timeout) = request(4000).await {
    println!("Request timed out!");
}
$> cargo run -- run scripts/book/async/async_http_timeout.rn
200 OK
Request timed out!

But wait, this is taking three seconds. We're not running the requests concurrently any longer!

Well, while the request and the timeout is run concurrently, the request function is run one at-a-time.

To fix this we need two new things: async functions and .await.

async functions

async functions are just like regular functions, except that when called they produce a Future.

In order to get the result of this Future it must be .awaited. And .await is only permitted inside of async functions and closures.

use std::future;

struct Timeout;

async fn request(timeout) {
    let request = http::get(`http://httpstat.us/200?sleep=${timeout}`);
    let timeout = time::sleep(time::Duration::from_secs(2));

    let result = select {
        _ = timeout => Err(Timeout),
        res = request => res,
    }?;

    Ok(result)
}

for result in future::join([request(1000), request(4000)]).await {
    match result {
        Ok(result) => println!("Result: {}", result.status()),
        Err(Timeout) => println!("Request timed out!"),
    }
}
$> cargo run -- run scripts/book/async/async_http_concurrent.rn
Result: 200 OK
Request timed out!

async closures

Closures can be prefixed with the async keyword, meaning calling them will produce a future.

fn do_request(url) {
    async || {
        Ok(http::get(url).await?.status())
    }
}

let future = do_request("https://google.com");
let status = future().await?;
println!("Status: {status}");
$> cargo run -- run scripts/book/async/async_closure.rn
Status: 200 OK

async blocks

Blocks can be marked with async to produce on-the-fly futures. These blocks can capture variables the same way as closures do, but take no arguments.

fn do_request(url) {
    async {
        Ok(http::get(url).await?.status())
    }
}

let future = do_request("https://google.com");
let status = future.await?;
println!("Status: {status}");
$> cargo run -- run scripts/book/async/async_blocks.rn
Status: 200 OK