Async

Asynchronous programming, or async for short, is a concurrent programming model supported by an increasing number of programming languages. It lets you run a large number of concurrent tasks, while preserving much of the look and feel of ordinary synchronous programming, through the async/await syntax. It helps you deal with events independently of the main program flow, using techniques like futures, promises, waiting, or eventing.

  • Ability to make progress on multiple tasks, even if they don't execute at the exact same time.
  • Mechanism: cooperative multitasking - tasks yield control, allowing other tasks to run.
  • Involves context switching on a single thread or, most often, among a few threads (the pool of which is opaquely managed by the async runtime).
  • Achieves non-blocking I/O operations to improve responsiveness and efficiency.
  • Lower overhead compared to multithreading.
  • Multi-threaded async programming also requires careful synchronization to prevent data races.

Key constructs in Rust:

Basic Example

cat-asynchronous

use std::future::Future;

struct SomeStruct;

// Most often, we will use async functions.
// Rust transforms the `async fn` at compile time into a state machine
// that _implicitly_ returns a `Future`. A future represents an
// asynchronous computation that might not have finished yet.
async fn first_task() -> SomeStruct {
    // ...
    SomeStruct
}

async fn second_task_1(_s: &SomeStruct) { // ...
}

// `async fn` is really syntaxic sugar for a function...
#[allow(clippy::manual_async_fn)]
fn second_task_2() -> impl Future<Output = ()> {
    // ...that contains an `async` block.
    async {} // returns `Future<Output = ()>`
}

async fn do_something() {
    // Use `.await` to start executing the future.
    let s = first_task().await;
    // `await` yields control back to the executor, which may decide to do
    // other work if the task is not ready, then come back here.

    // `join!` is like `.await` but can wait for multiple futures
    // concurrently, returning when all branches complete.
    let f1 = second_task_1(&s);
    let f2 = second_task_2();
    futures::join!(f1, f2); // or tokio::join!
}

// We replace `fn main()` by `async fn main()` and declare which
// executor runtime we'll use - in this case, Tokio. The runtime crate
// must be added to `Cargo.toml`: `tokio = { version = "1", features =
// ["full"] }` Technically, the #[tokio::main] attribute is a macro
// that transforms it into a synchronous fn main() that initializes a
// runtime instance and executes the async main function.
#[tokio::main]
async fn main() {
    do_something().await;
    // note: `await` must be called or nothing is executing.
    // Futures are lazy
}

As any form of cooperative multitasking, a future that spends a long time without reaching an await⮳ "blocks the thread", which may prevent other tasks from running.

Differences with other languages

Rust's implementation of async⮳ differs from most languages in a few ways:

  • Rust's async⮳ operations are lazy. Futures are inert in Rust and only make progress only when polled. The executor calls the std::task::Poll⮳ method repeatedly to execute futures.
async fn say_world() {
    println!("world");
}

#[tokio::main]
async fn main() {
    // Calling `say_world()` does not execute the body of `say_world()`.
    let op = say_world();

    // This println! comes first
    println!("hello");

    // Calling `.await` on `op` starts executing `say_world`.
    op.await;
}
// Prints:
// hello
// world
// Example from https://tokio.rs/tokio/tutorial/hello-tokio
  • Dropping a future stops it from making further progress.
  • Async is zero-cost in Rust. You can use async⮳ without heap allocations and dynamic dispatch. This also lets you use async in constrained environments, such as embedded systems.
  • No built-in runtime is provided by Rust itself. Instead, runtimes are provided by community-maintained crates.
  • Both single- and multi-threaded runtimes are available.

Which crate provides what?

  • The async⮳ / await⮳ syntactic sugar is supported directly by the Rust compiler.
  • The most fundamental traits, types, and functions, such as the std::future::Future⮳ trait, are provided by the standard library.
  • Many utility types, macros and functions are provided by the futures⮳ crate. They can be used in any async Rust application.
  • Execution of async code, IO and task spawning are provided by "async runtimes", such as tokio⮳ and async_std. Most async applications, and some async crates, depend on a specific runtime.

Async runtimes

async-std smol embassy mio cat-asynchronous

In most cases, prefer the tokio runtime - see The State of Async Rust: Runtimes⮳.

Alternatives to the Tokio async ecosystem include:

  • async-std async_std-crates.io⮳: async version of the Rust standard library. No longer maintained?
  • smol Smol
  • embassy Embassyembassy-github for embedded systems.
  • mio Mio⮳ is a fast, low-level I/O library for Rust focusing on non-blocking APIs and event notification for building high performance I/O apps with as little overhead as possible over the OS abstractions. It is part of the Tokio ecosystem.

See also

Are we async yet?

Asynchronous Programming in Rust (book)