#Rust

One Page of Async Rust: Tony Finch's Deep Dive into Futures and Wakers

Tech Essays Reporter
3 min read

Tony Finch explores the fundamentals of async Rust by building a minimal async executor from scratch, revealing the surprisingly compact core concepts behind Rust's async/await system while grappling with the complexities of Pin, Context, and Waker

Tony Finch's exploration of async Rust reveals that beneath the surface complexity lies a remarkably compact foundation. His journey began with a simple simulation problem: tasks that progress through steps with delays, sharing state, all running in virtual time without actual sleeping. Rather than reaching for an existing crate, Finch chose to understand the lower-level mechanics of async Rust, discovering that the core boilerplate could fit on a single page of printed text.

The adventure starts with the basics: an async function like async fn deep_thought() -> u32 { 42 } doesn't execute immediately but returns a Future<Output = u32>. The compiler transforms this into a state machine, but nothing runs until you explicitly poll it. This leads to the first fundamental concept: Pin. Unlike normal Rust data structures that can be moved freely, futures can be self-referential, containing pointers to their own internal state. Pin prevents movement, ensuring memory safety. Finch wraps futures in Box::pin() and creates a simple Task struct to manage them.

The Future::poll() method signature immediately introduces complexity: fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Self::Output>. The Context contains a Waker, which is how async primitives communicate with the executor. Starting with Waker::noop(), Finch demonstrates that even this minimal setup can run an async function, though it lacks any real coordination.

The heart of async programming lies in primitive operations. When an async function calls another async function and awaits it, the compiler generates poll calls down to primitive futures. These primitives typically follow a pattern: first poll arranges for the operation and returns Poll::Pending, suspending the task; later, when the operation completes, the executor polls again and receives Poll::Ready(). For his simulation, Finch creates a minimal Sleep future that suspends for a specified duration, demonstrating how state machines can be encoded in simple data structures.

The Waker system proves to be the most challenging aspect. Rust's design requires constructing a RawWaker from a raw pointer and a virtual function table, which feels like "hand-rolled object-oriented C" rather than idiomatic Rust. The intent is clear: a waker should be a smart pointer to the current task, allowing primitive futures to arrange operations and later wake the task when complete. However, the unsafe nature of this interface and the difficulty of managing task references led Finch to an alternative approach.

Instead of using wakers for wake-up notifications, Finch repurposes the Context as a side-channel for commands. His Yield enum combines both primitive commands (like Sleep) and the Poll::Ready() state. When a primitive future wants to perform an operation, it overwrites the command in the context before returning Poll::Pending. The executor loop then interprets these commands to decide what to do with the task.

The complete system demonstrates fake time simulation elegantly. Tasks are managed in a min-heap priority queue keyed by wake-up time. The executor loop pops tasks, polls them, and based on the yielded command either reschedules them (for sleep) or drops them (when done). The demo shows multiple activities sleeping for different intervals, printing their progress synchronously to illustrate the state machine progression.

Several questions emerge from this exploration. Why isn't Waker a simple trait parameter instead of requiring raw pointers and unsafe code? The standard library's partial restriction on waker shape seems unnecessary given that async runtimes could handle the details. Finch's unsafe code, while passing Miri's checks, raises concerns about whether the compiler fully understands the lifetime relationships between the yielded value and the poll operation.

The exercise reveals that async Rust's core is surprisingly minimal: futures, pinning, contexts, and wakers form the essential foundation. The complexity comes from the abstractions built on top and the safety requirements around self-referential data structures. For developers building custom async runtimes or understanding how async/await works under the hood, this one-page distillation provides valuable insight into the fundamental mechanisms that power Rust's async ecosystem.

This exploration serves as both a practical demonstration and a philosophical inquiry into Rust's async design choices. It shows that while the surface area of async Rust appears vast, the core concepts are compact enough to fit on a single page—though mastering their interactions and safety implications requires much deeper study.

Comments

Loading comments...