Async/await using tokio and Rust

Rust supports async/await in a similar way to C# although these are supplied via runtimes, for example Tokio, async-std and others.

In this post we’ll look at the tokio runtime option.

The first thing we need to do is add tokio to the Cargo.toml, for example

[dependencies]
tokio = { version = "1", features = ["full"] }

Now, let’s create a simple async function

async fn execute() {
   println!("Execution in async function");
}

Notice we do not return a Task like C# or any type in this case, but this is essentially syntactic sugar for

fn execute() -> impl Future<Output = ()> 

Hence, we can see async functions return a Future (similar to a Promise in Javascript etc.).

The Future trait has a poll function which can be checked to see if the async function is ready to return a value or if it’s pending.

To await an async function we use te following syntax

execute().await;

The await will ofcourse cause the current Future to return to the caller but the code after the await will not execute until the Future completes/is ready.

If you come from C# this is much the same, i.e. running a continuation when completed etc.

To use asyc/await on main we need to make a couple of changes, first to make main async but this alone will not work without the runtime, hence main looks like this

#[tokio::main]
async fn main() {
  execute().await;
}

Futures are lazy loaded. Meaning, that the future will not execute until the await is called.

As you can see Futures do not run in a thread, they are just polling futures. However we can use tokio tasks (which looks a lot like std lib threads) the execute the code on a thread

 
let handle = tokio::spawn(asyc move {
   execute().await;
}

handle.await.unwrap();

By default tokio executes on a threadpool but we could change things, as below

#[tokio::main(flavor = "current_thread")]

Which then uses time slicing instead of threads.

Tokio is good for non blocking IO, but tokio uses a single thread for it’s main event loop hence heavy CPU will basically slow down other tasks. Hence we would need to spawn threads as already discussed.

Slight detour

As a slight detour from async/await – tokio can also create “green” threads (lightweight threads from the runtime – not OS threads), for example

async fn execute() {
   time::sleep(time::Duration::from_secs(1)).await;
}

fn main() {
  let runtime = tokio::runtime::Runtime::new().unwrap();
  
  let future = execute();

  runtime.block_on(future);
}