Lazy async operations in Rust

async and await

JavaScript developers may feel at home with Rust's concurrency model, which uses the async/await concept. In JavaScript we have Promise to represent eventual results of async operations, and in Rust we have Future.

The following shows an example of async and await syntax in JavaScript and Rust code:

JavaScript

// greet.js

async function main() {
  await greet("Hi there!");
}

async function greet(message) {
  console.log(message);
}

/*
Output:

Hi there!
*/

Rust

// greet.rs

#[tokio::main]
async fn main() {
    greet("Hi there!").await;
}

async fn greet(message: &str) {
    println!("{}", message);
}

/*
Output:

Hi there!
*/

Currently, Rust does not include an async runtime out-of-the-box. We use the Tokio runtime in our examples.

Executing async operations without suspending the running function

Now, consider a scenario where we want to perform an async side-effect, such as dispatching a message, but we do not want to suspend the running function until this side-effect completes.

In JavaScript we can call the publish function, which returns a Promise, without needed to await it. setTimeout queues the given callback on the event loop and immediately returns a Promise. The main function then logs "Don't block me!" to the console. Finally the event loop eventually runs our callback, which logs "Executed main()".

// publish.js

async function main() {
  publish("Executed main()"); // no await
  console.log("Don't block me!");
}

// returns a Promise
function publish(message) {
  return setTimeout(() => {
    console.log(message);
  }, 0);
}

/*
Output:

Don't block me!
Executed main()
*/

Things look different in Rust when we do not await the Future returned by publish.

// publish.rs

#[tokio::main]
#[allow(unused_must_use)]
async fn main() {
    publish("Executed main()");  // no await
    println!("Don't block me!");
}

async fn publish(message: &str) {
    println!("{}", message);
}

/*
Output:

Don't block me!
*/

For some reason, the Rust program output does not show "Executed main()"! According to the Rust documentation on Future, "futures do nothing unless you .await them". Unlike JavaScript, Rust's async operations are lazy.

In Rust we must use .await on the Future to execute the async operation. However awaiting the publish causes the main function to suspend and prevent execution of println!("Don't block me!") until after publish completes. To avoid this situation and achieve a similar behaviour to JavaScript's Promise handling, we can use tokio::spawn.

// publish.rs

#[tokio::main]
async fn main() {
    // Spawns a new async task, and executes it concurrently
    tokio::spawn(async {
        publish("Executed main()").await;
    });

    println!("Don't block me!");
}

async fn publish(message: &str) {
    println!("{}", message);
}

/*
Output:

Don't block me!
Executed main()
*/

It works as we intended! tokio::spawn executes publish concurrently without blocking the remaining lines operations in main!

33