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 .await
ed. 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