post thumbnail

Analysis of Rust Async Principles

Rust's async/await implements zero-cost asynchronous programming through the Future trait, compiling async code into state machines driven by runtimes like tokio via poll execution. Key features include: 1) No hidden performance overhead; 2) Dependency on community-provided runtimes; 3) Support for both multi-threaded and single-threaded scheduling. Developers can write asynchronous code in synchronous style, while the runtime automatically suspends blocking tasks and switches execution between Futures. Understanding its state machine mechanism helps optimize asynchronous workflows.

2025-07-14

The previous article Asynchronous Programming async introduced how to use Rust’s async. Today’s article will further explain how async is implemented in Rust, helping us better utilize its features in practice by understanding its underlying principles.

Introduction to Rust async/await

async/await is built-in syntax in Rust, similar to the go syntax in Golang, allowing us to write asynchronous logic in a synchronous style.

However, its implementation differs from other languages in several key ways:

The underlying implementation of async in Rust is complex. This article focuses on key principles in its implementation. async is considered syntactic sugar—Rust compiles async-marked blocks into state machines that implement the Future trait. These are then executed by a thread in the asynchronous runtime. When a Future is blocked, it yields control of the current thread, allowing other Futures to execute on the same thread. This enables asynchronous calls without blocking the thread.

Thus, async/await relies on the following components:

Let’s look at a simple demo to get a feel for it:

TOML# [dependencies]
tokio = { version = "1.45.0", features = ["full"] }

The code in main.rs is as follows:

RUSTuse tokio::time::{sleep, Duration};

async fn eat_dish() {
    println!("eating dinner.");
    sleep(Duration::from_secs(1)).await;
}

#[tokio::main]
async fn main() {
    eat_dish().await;
    println!("Finished!");
}

Principle Analysis

The code uses asyncawait, and tokio to implement an asynchronous feature.

First, async fn eat_dish is compiled by the Rust compiler into a state machine that implements the Future trait. The definition of Future in the source code is as follows:

RUSTpub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

The desugared version of async fn eat_dish would look something like this:

RUST// async fn eat_dish() is desugared into:
struct EatDishFuture {
    state: State, // states of a manually implemented state machine
}

impl Future for EatDishFuture {
    type Output = (); // associated type

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
        match self.state {
            State::Start => {
                println!("eating dinner.");
                let sleep_fut = sleep(Duration::from_secs(1));
                self.state = State::Sleeping(sleep_fut);
                Poll::Pending
            }

            State::Sleeping(ref mut sleep_fut) => {
                // sleep_fut returning Ready upon polling indicates the timer has finished.
                match Pin::new(sleep_fut).poll(cx) {
                    Poll::Pending => Poll::Pending,
                    Poll::Ready(()) => Poll::Ready(()),
                }
            }
        }
    }
}

Next, await is transformed by the compiler into calls to poll() and state managementNote that the await method can only be called within async functions.

The main function is annotated with the #[tokio::main] macro, which starts a tokio runtime, initializes the task scheduler, and executes the main() async state machine.

The execution flow is as follows: the tokio runtime calls async main, encounters eat_dish().await, and invokes EatDishFuture.poll. Since eat_dish() includes a sleep to simulate time-consuming business logicEatDishFuture is blocked and suspended, allowing the main thread to execute other Futures (though here there’s only one EatDishFuture). The main thread then repeatedly calls poll on all suspended Futures. Once EatDishFuture completes, it executes println!("Finished!"); in async main and finally terminates the process. The entire process is zero-allocation, non-blocking, and entirely driven by poll.

Custom EatDishFuture

As mentioned earlier, the Rust compiler compiles async fn eat_dish into a struct that implements the Future trait. Below, we manually implement this Future struct to observe its behavior. The code is as follows:

RUSTuse std::pin::Pin;
use std::task::{Context, Poll};
use std::future::Future;
use tokio::time::{sleep, Duration, Sleep};

enum State {
    Start,
    Sleeping(Pin<Box<Sleep>>),
    Done,
}

struct EatDish {
    state: State,
}

impl EatDish {
    fn new() -> Self {
        Self {
            state: State::Start,
        }
    }
}

impl Future for EatDish {
    type Output = ();
    
    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        loop {
            match &mut self.state {
                State::Start => {
                    println!("eating dinner.");
                    let sleep_future = Box::pin(sleep(Duration::from_secs(1)));
                    self.state = State::Sleeping(sleep_future);
                }
                State::Sleeping(fut) => {
                    // manually poll the sleep future
                    match fut.as_mut().poll(cx) {
                        Poll::Pending => return Poll::Pending,
                        Poll::Ready(()) => {
                            self.state = State::Done;
                        }
                    }
                }
                State::Done => {
                    return Poll::Ready(());
                }
            }
        }
    }
}

#[tokio::main]
async fn main() {
    EatDish::new().await;
    println!("Finished!");
}

The runtime behavior is identical to the previous example. Here, we define a custom EatDish struct that implements the Future trait, using a state machine to manage execution states. This achieves the same functionality as async fn eat_dish. Finally, calling EatDish::new().await within tokio::main enables asynchronous execution.

Summary

Rust’s async/await asynchronous programming is based on the Future trait, where async fn blocks are compiled into structs that implement Future. This elegant approach combines Rust’s powerful type system with zero-cost abstractions, delivering a high-performance and safe asynchronous programming model.

By compiling async fn into state machines and driving their execution via poll in the runtime, Rust achieves an efficient, non-blocking asynchronous scheduling model. While Rust’s implementation is more low-level and complex compared to other languages, it offers greater performance advantages and flexibility.

In practice, since Rust does not provide a built-in runtime for async/await, community-developed runtimes like tokio and async-std are essential for simplifying task scheduling and execution. Understanding the underlying principles not only helps in using async/await more effectively but also provides insights for troubleshooting performance bottlenecks or complex asynchronous workflows.

Building on the foundation of Asynchronous Programming async, this article analyzed Rust’s async programming model from a principles perspective. Future articles will explore how async can be leveraged in specific scenarios to maximize its potential.