BudiBadu Logo
Samplebadu

Rust by Example: Futures and Await

Rust 1.75+

Demystify the machinery behind async/await. This advanced example explores the Future trait, the polling mechanism, and how executors drive futures to completion.

Code

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use futures::executor::block_on;

struct Delay {
    when: Instant,
}

// Implementing a custom Future
impl Future for Delay {
    type Output = &'static str;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if Instant::now() >= self.when {
            Poll::Ready("Time's up!")
        } else {
            // Tell the waker to wake us up later.
            // In a real implementation, we would register a timer with the OS.
            // Here we just spin (busy wait) which is bad practice but simple for demo.
            cx.waker().wake_by_ref(); 
            Poll::Pending
        }
    }
}

async fn demo() {
    let when = Instant::now() + Duration::from_millis(100);
    let future = Delay { when };
    
    let out = future.await;
    println!("{}", out);
}

fn main() {
    block_on(demo());
}

Explanation

At the heart of Rust's async model is the Future trait. A Future represents a value that might not be available yet. The core method of the trait is poll. An executor calls poll repeatedly to check if the future is ready.

poll returns an enum Poll::Ready(T) or Poll::Pending. If it returns Pending, it means the future is waiting on something (like a timer or network packet). Crucially, it also registers a Waker. When the external event happens, the Waker is used to tell the executor "I'm ready to make progress, poll me again!"

Most developers don't implement Future manually. Instead, they use async blocks and functions, which the compiler compiles into state machines that implement Future for you. However, understanding polling is key to understanding how Rust achieves zero-cost async abstractions.

Code Breakdown

15
fn poll(...). The signature takes Pin<&mut Self>. Pinning is a complex topic, but essentially it ensures the future doesn't move in memory, which is required for self-referential structs generated by async/await.
22
cx.waker().wake_by_ref(). This signals the executor to poll this task again immediately. In a real timer, we wouldn't wake immediately; we'd ask the OS to wake us after the duration passes.
32
future.await. The compiler desugars this into a loop that calls poll. If poll returns Pending, it yields. If Ready, it breaks the loop and returns the value.
19
Poll::Ready("Time's up!"). The final state. Once a future returns Ready, it should not be polled again.