Rust Stabilizes Async-Await to Deliver Zero-Cost Network IO

RustLatam 2019 - Without Boats: Zero-Cost Async IO — Rust Programming Language

Rust Programming Language

By pairing poll-based futures with the new Pin pointer to secure self-referential structures, Rust delivers ergonomic asynchronous programming that matches the performance of hand-written state machines.

AI-assisted summary of RustLatam 2019 - Without Boats: Zero-Cost Async IO — original source. Check the original for full context.

Source published:

Key ideas

  • Rust abandoned its pre-1.0 green threads architecture because runtime-enforced schedulers imposed hidden overhead on all binaries and crippled seamless C interoperability, violating the principles of zero-cost abstractions. (7:36)
  • Unlike callback-driven futures in other languages that demand pervasive heap allocations, Alex Crichton and Aaron Turon's poll-based future model enables the compiler to flatten async workflows into single-allocation enum state machines. (13:40)
  • Holding references across await points generates self-referential structs that break under memory moves; the introduction of the Pin type solved this by guaranteeing immovable memory addresses without runtime cost. (26:17)
  • Following the stabilization of Pin in Rust 1.33, the core Future trait and async/await syntax were slated for complete stabilization by mid-to-late 2019, paving the way for asynchronous Streams and general generators. (29:41)

Detailed notes

## Background Published on YouTube by the Rust Programming Language channel on April 22, 2019, this talk from RustLatam 2019 features without boats, a Senior Research Engineer at Mozilla Research and core member of the Rust language design team. According to the description, boats—who contributed to major Rust capabilities like generic associated types and const generics—focuses on the year-long design effort behind bringing first-class async/await syntax to non-blocking I/O in Rust.

## TL;DR In this keynote, without boats examines how Rust achieved zero-cost asynchronous I/O without sacrificing low-level systems control or requiring a heavy runtime. Boats charts the evolution from discarded pre-1.0 green threads and flawed callback-based futures to Alex Crichton and Aaron Turon's poll-based state machine model. The talk highlights the major usability crisis caused by self-referential structs when borrowing across suspension points and explains how the invention of the `Pin` type resolved this issue. Finally, boats presents the 2019 stabilization roadmap for async/await alongside future ambitions for async traits, streams, and language-level generators.

## Atmosphere & style The presentation possesses an earnest, lucid, and technically rigorous tone, delivered with unpretentious charm even as boats works through a slight cold. Candid reflections on community exasperation—such as early compiler error soup and users wishing Rust had garbage collection—give the architectural deep dive relatable emotional weight. The pacing feels collaborative and reflective of open-source language engineering at its best, concluding with an endearing, self-deprecating laugh over a minor slide glitch that garners warm applause.

## Bulletin - [07:36](https://www.youtube.com/watch?v=skos4B5x7qE&t=456s) Rust abandoned its pre-1.0 green threads architecture because runtime-enforced schedulers imposed hidden overhead on all binaries and crippled seamless C interoperability, violating the principles of zero-cost abstractions. - [13:40](https://www.youtube.com/watch?v=skos4B5x7qE&t=820s) Unlike callback-driven futures in other languages that demand pervasive heap allocations, Alex Crichton and Aaron Turon's poll-based future model enables the compiler to flatten async workflows into single-allocation enum state machines. - [26:17](https://www.youtube.com/watch?v=skos4B5x7qE&t=1577s) Holding references across await points generates self-referential structs that break under memory moves; the introduction of the Pin type solved this by guaranteeing immovable memory addresses without runtime cost. - [29:41](https://www.youtube.com/watch?v=skos4B5x7qE&t=1781s) Following the stabilization of Pin in Rust 1.33, the core Future trait and async/await syntax were slated for complete stabilization by mid-to-late 2019, paving the way for asynchronous Streams and general generators.

## Key takeaways - [01:55](https://www.youtube.com/watch?v=skos4B5x7qE&t=115s) The async/await feature set introduces two keywords—async and await—transforming functions into state machines returning Future types while retaining the linear ergonomics of blocking code. - [04:26](https://www.youtube.com/watch?v=skos4B5x7qE&t=266s) Rust adheres to Bjarne Stroustrup's definition of zero-cost abstractions: you do not pay for what you do not use, and hand-written low-level code cannot outperform the abstraction. - [05:59](https://www.youtube.com/watch?v=skos4B5x7qE&t=359s) Standard blocking I/O fails to scale under massive network connection counts due to operating system thread overhead and kernel context switching costs. - [07:36](https://www.youtube.com/watch?v=skos4B5x7qE&t=456s) Green threads were completely removed prior to Rust 1.0 because bundling a mandatory userspace scheduler imposed an unavoidable runtime footprint that blocked embedding Rust into existing C programs. - [10:35](https://www.youtube.com/watch?v=skos4B5x7qE&t=635s) Standard callback-based futures failed Rust's zero-cost guarantees because each chained step required dynamic dispatch and separate heap allocations (Box<dyn FnOnce>). - [13:40](https://www.youtube.com/watch?v=skos4B5x7qE&t=820s) The poll-based model inverts control: executors drive progress by actively polling futures, and reactors notify executors via Wakers only when underlying I/O events become ready. - [13:40](https://www.youtube.com/watch?v=skos4B5x7qE&t=820s) Poll-based futures offer trivial task cancellation, as simply dropping the future handle halts further execution without requiring cancellation tokens. - [13:40](https://www.youtube.com/watch?v=skos4B5x7qE&t=820s) Compiling poll-based workflows collapses nested stages into an enum-like state machine that shares memory across variants, requiring only a single heap allocation when spawned. - [18:43](https://www.youtube.com/watch?v=skos4B5x7qE&t=1123s) Early future combinators (like and_then and map) created deeply nested closures and sprawling type signatures that triggered impenetrable compiler diagnostic errors. - [21:09](https://www.youtube.com/watch?v=skos4B5x7qE&t=1269s) Awaiting a future desugars into a polling loop that yields Pending to the outer executor until a wake notification signals resumption. - [23:08](https://www.youtube.com/watch?v=skos4B5x7qE&t=1388s) Holding a reference across an await suspension point causes the generated state machine struct to hold a pointer to its own fields, creating a self-referential struct. - [23:08](https://www.youtube.com/watch?v=skos4B5x7qE&t=1388s) Because Rust standard moves use bitwise copies (memcpy) that leave internal pointers dangling, early futures strictly prohibited borrowing across await points. - [26:17](https://www.youtube.com/watch?v=skos4B5x7qE&t=1577s) The Pin pointer wrapper prevents values from moving in memory, allowing safe borrowing across await points without heap indirection or runtime overhead. - [26:17](https://www.youtube.com/watch?v=skos4B5x7qE&t=1577s) Rewriting Future::poll to accept Pin<&mut Self> instead of &mut Self anchored async state machines to fixed memory addresses, unlocking idiomatic Rust borrowing in async functions. - [29:41](https://www.youtube.com/watch?v=skos4B5x7qE&t=1781s) Following async/await stabilization, the Rust roadmap prioritized the Stream trait for multi-value asynchronous iteration, async methods in traits, and general language generators.

## Detailed walkthrough ### [00:07](https://www.youtube.com/watch?v=skos4B5x7qE&t=7s) Introduction and Welcoming the Global Rust Community

The talk opens with an introduction from without boats, a Senior Research Engineer at Mozilla working full-time on Rust: “I'm my name is without boats as in yeah see about this you can most people call me boats which is actually even more confusing I am researcher at Mozilla I work on rust my full-time job it's pretty cool I think.” Boats expresses deep appreciation to the organizers of RustLatam 2019, pointing out that this conference marks a major milestone: it is believed to be the very first Rust conference held outside of the United States or Europe. Boats highlights how thrilling it is to see the global Rust community thriving, expanding, and launching new events across the world before transitioning into the technical material.

The focus of the talk is `async`/`await`, a language feature boats has been working on directly for about a year and a half, building upon efforts that community members have pursued since the earliest pre-1.0 days of Rust. Slated to ship within the upcoming months of 2019, `async`/`await` represents arguably the biggest addition to Rust that year, offering a long-sought zero-cost abstraction for asynchronous I/O.

[01:55](https://www.youtube.com/watch?v=skos4B5x7qE&t=115s) The Mechanics of `async`/`await`

Boats explains that at a language level, the feature introduces two keywords: `async` and `await`. Marking a function with the `async` modifier fundamentally alters its behavior. Rather than executing sequentially to completion and returning its value immediately when called, an async function returns immediately with a `Future` that will eventually resolve to the declared return value. Inside an async function, developers can use the `await` operator on other futures. Applying `await` pauses execution of the current function until the awaited future becomes ready, making concurrent, non-blocking workflows far simpler to write than manual state machines or callback chains.

To illustrate, boats presents a toy example of an ORM-style database query helper:

```rust async fn get_user(username: &str) -> User { let query_str = format!("SELECT * FROM users WHERE name = '{}'", username); let response = db.query(&query_str).await; User::parse(response) } ```

In this pattern, calling `db.query(...)` initiates asynchronous I/O and yields a future. Adding `.await` suspends execution until the response arrives, at which point the domain object `User` is parsed and returned. Boats notes that the only visible difference between this and traditional blocking code is the addition of the `async` qualifier and the `.await` expressions:

- `async` modifies function signatures to return a `Future` instead of computing the result synchronously. - `await` suspends execution within an async function until the target future completes. - The ergonomics closely mimic blocking, sequential code, eliminating callback spaghetti without giving up non-blocking performance.

[04:26](https://www.youtube.com/watch?v=skos4B5x7qE&t=266s) The Philosophy of Zero-Cost Abstractions

The central achievement of Rust's async/await and futures model is that it qualifies as a genuine zero-cost abstraction. Boats emphasizes that this philosophy is Rust's defining trait and sets it apart from the majority of programming languages. While the concept did not originate with Rust, boats invokes Bjarne Stroustrup's canonical definition from C++ to explain the dual requirements of the principle:

> “a zero cost attraction means that when you don't use it you don't pay for it and further when you do use it you couldn't hand coded any better” — without boats

Boats unpacks both rules that every zero-cost abstraction must satisfy:

1. **No hidden global overhead:** A feature cannot impose performance or memory penalties on code paths or users who do not utilize it; it must not burden the entire language or slow down unrelated programs. 2. **Optimal handwritten parity:** When an engineer chooses to use the abstraction, it must compile down to machine code so efficient that writing it by hand with lower-level primitives would yield no meaningful performance gain. If developers feel compelled to write tedious low-level code by hand just to avoid a slow convenience feature, the abstraction has failed.

[05:59](https://www.youtube.com/watch?v=skos4B5x7qE&t=359s) The Dilemma of Blocking vs. Asynchronous I/O

Boats moves to the specific technical challenge: I/O scalability. Standard operating system I/O primitives are blocking. When a thread issues a read or write, the OS puts the thread to sleep, switching execution contexts until data is ready. While simple to reason about, blocking I/O fails to scale under high-concurrency network workloads where a service must maintain tens of thousands or millions of concurrent client connections.

Because Rust targets systems programming where computing resources and low-level control are paramount, having a competitive networking story is mandatory: “and so for us to really be successful in the network space we really need some sort of solution to this asynchronous i/o problem”. However, non-blocking operating system calls invert control: they return immediately, placing the responsibility squarely on the user's program to manage event loops, track state, and schedule callbacks when sockets become readable or writable. Managing that bookkeeping manually adds immense application complexity.

```mermaid flowchart TD accTitle: Blocking vs Non-Blocking IO Scalability accDescr: Comparison of operating system thread scheduling under blocking IO versus event-driven resumption under async IO.

subgraph Blocking["Blocking I/O"] B1["Thread issues I/O syscall"] --> B2["OS puts thread to sleep"] B2 --> B3["Heavy kernel context switch"] B3 --> B4["Fails at high concurrency"] end

subgraph Async["Asynchronous I/O"] A1["Syscall returns immediately"] --> A2["User code continues other work"] A2 --> A3["Program tracks readiness & callbacks"] A3 --> A4["Scales to high connection counts"] end ```

[07:36](https://www.youtube.com/watch?v=skos4B5x7qE&t=456s) The Rise and Fall of Rust's Green Threads

Early in its development, Rust addressed scalable I/O by implementing an M:N runtime based on "green threads," an architecture familiar from languages like Go (goroutines) or early Java. Under this model, application code still looks like regular blocking I/O: developers spawn threads and perform blocking calls, but these threads are lightweight userspace abstractions managed by a language runtime rather than native OS threads.

Green threads provide significant wins over native OS threads for high-volume network servers: - **Stack size and memory:** Native OS threads allocate fixed, multi-megabyte stacks upon creation. Green threads initialize with tiny stacks that grow dynamically, making it feasible to spawn hundreds of thousands of concurrent tasks without running out of memory. - **Context switching costs:** Switching between green threads takes place entirely in userspace memory within the application process, avoiding the heavy performance hit of kernel context switches.

Despite these benefits, Rust abandoned green threads shortly before its 1.0 release. The model fundamentally violated the first rule of zero-cost abstractions: it imposed costs on everyone, even those who never used green threads. Every Rust program was forced to carry a heavy runtime to manage the userspace scheduler.

This became an immediate barrier for one of Rust's primary adoption strategies: embedding Rust libraries into larger, existing C software architectures. Requiring a runtime environment to be initialized before calling any Rust function made incrementally replacing C components prohibitive. To keep Rust truly systems-level with a runtime footprint no heavier than C, green threads were excised from the language prior to 1.0.

[10:35](https://www.youtube.com/watch?v=skos4B5x7qE&t=635s) Futures and the Failure of Callback-Based Async

Rust's decision to remove green threads left native C-interoperability fast and cheap, preserving one of the language's core strengths. However, eliminating runtime-level green threads meant that asynchronous I/O still required a robust solution. The design constraint was clear: it could not be baked into a mandatory language runtime bundled with every compiled binary. Instead, Rust needed a purely library-based abstraction that programmers could opt into only when necessary.

Across the broader programming language landscape, the dominant paradigm for asynchronous values was the future—also known as a promise in JavaScript. A future represents an eventual value that may not have evaluated yet, allowing a caller to construct pipelines and combinators or use `async`/`await` syntax before the operation actually finishes. This pattern generalizes cleanly beyond network I/O to include timers or heavy CPU-bound tasks delegated to a thread pool.

```mermaid flowchart LR accTitle: Callback-based Future Allocation Problem accDescr: Diagram illustrating how scheduling multiple callbacks requires heap-allocating boxed trait objects for each step. subgraph CallbackChain ["Callback Model: Repeated Allocations"] direction LR A["Network Request Future"] -->|"schedule callback"| B["Box&lt;dyn FnOnce&gt;"] B -->|"schedule next step"| C["Box&lt;dyn FnOnce&gt;"] C -->|"schedule next step"| D["Box&lt;dyn FnOnce&gt;"] end ```

The standard implementation of futures in other languages relied on callbacks, where an asynchronous handle registers a closure to be executed whenever the underlying event resolves. In Rust, early experimentation demonstrated that this callback-driven approach broke down catastrophically. As Without Boats puts it:

> “This like approach failed the zero cost abstraction on the second principle where now you were it was not affecting people who work using it but if you did use it it would just be way slower than if you just had an end written something yourself and so why would you use it because if you wrote the thing yourself would be much faster”

- Callbacks demanded pervasive heap allocations, requiring each scheduled step to become a separate boxed trait object. - The proliferation of heap allocations introduced dynamic dispatch penalties everywhere. - The performance degradation violated Rust's zero-cost abstraction promise by running substantially slower than hand-crafted state machines.

[13:40](https://www.youtube.com/watch?v=skos4B5x7qE&t=820s) The Poll-Based Alternative: Alex Crichton and Aaron Turon's Model

Struggling through a slight cold, Without Boats credited Alex Crichton (who was present in the room) and Aaron Turon for designing a fundamentally different, poll-based future abstraction. Rather than having a future actively schedule a callback upon completion, the relationship is inverted: an external runtime component called an executor drives progress by repeatedly polling the future. When polled, the future either returns `Ready` with its resulting value or `Pending` if it cannot yet finish.

```mermaid flowchart TD accTitle: Rust Poll-Based Event Cycle accDescr: Cycle between Executor and Reactor during the lifecycle of a polled future until resolution. subgraph Lifecycle ["Poll-Based Async Cycle"] direction TD E["Executor"] -->|"1. poll(waker)"| F["Future State Machine"] F -->|"2. Pending (needs I/O)"| R["Reactor"] R -->|"3. I/O completes: wake()"| E F -.->|"4. Ready(T)"| D["Drop Handle & Complete"] end ```

This inverted control model introduced significant structural benefits:

- **Effortless Cancellation:** Canceling a task requires nothing more than abandoning the future. Because progress only happens during an active `poll` call, dropping the handle halts execution immediately, avoiding the messy cancellation tokens common to callback architectures. - **Decoupled Architecture:** Instead of hardwiring a global event loop, the poll model establishes clean boundaries separating the *executor* (which schedules tasks), the *reactor* (which registers system I/O events), and user application logic. Systems developers can swap either component independently based on specific performance demands. - **Single-Allocation State Machines:** The compiler flattens an asynchronous workflow into an `enum`-like state machine representation. Each pause point (such as waiting on an I/O event) forms a variant containing strictly the state variables required to resume execution from that point, overlaid across an internal union.

The state machine requires only a single heap allocation when spawned, containing the variant discriminant and the union of its possible execution states. This completely eliminated the chain of boxed callback allocations.

[16:52](https://www.youtube.com/watch?v=skos4B5x7qE&t=1012s) The Executor-Reactor Lifecycle and Early Benchmark Results

To clarify how the executor, reactor, and future interact in practice, Without Boats broke down the runtime sequence visually. When a task is initially spawned, it is allocated in a single heap location and its handle is registered with the executor. The executor polls the future, running native compiled code until an I/O operation is required. At that moment, the future delegates registration to the reactor, supplying a `Waker`.

The reactor monitors the operating system for the completion of the requested I/O event. When the event fires, the reactor invokes the `Waker`, passing notification back to the executor that the task is ready to make forward progress. The executor resumes polling, repeating this handoff loop until the future eventually yields a final ready value. Once resolved, the executor drops the handle and the allocated state machine is cleanly deallocated.

When this architecture was initially benchmarked against asynchronous implementations across multiple other programming languages, the Rust prototype achieved market-leading throughput. As Boats highlighted, Rust's implementation sat on the far-left edge of the benchmark chart, demonstrating that zero-cost abstractions could remain fully competitive with the fastest hand-tuned asynchronous runtimes available.

[18:43](https://www.youtube.com/watch?v=skos4B5x7qE&t=1123s) From Combinator Chains to the First Async/Await Syntax

Even though poll-based state machines yielded exceptional runtime efficiency, hand-writing nested `enum` state machines across an entire application was unworkable. To solve the ergonomics problem without discarding zero-cost guarantees, the Rust community initially introduced futures combinators, patterned directly after iterator adapters like `map`, `filter`, and `and_then`.

Under this style, an asynchronous operation fetching `rust-lang.org` and decoding the response into a string had to chain combinator closures together:

```rust // Conceptual pattern: Chaining combinators on a future fetch_url("https://www.rust-lang.org") .and_then(|response| response.into_string()) .map(|body| println!("{}", body)) ```

While combinators avoided raw state machine authoring, deeply nested chains of closures introduced syntactic noise and made complex control flow hard to read. To restore straight-line readability, the ecosystem began prototyping `async`/`await` syntax.

The earliest edition was delivered not as a built-in compiler feature, but as an experimental library utilizing procedural syntax macros. This macro transformed ordinary sequential-looking code back into a single unified future. Rather than scattering nested closures, the programmer wrote straight-line logic annotated with `async` and `.await`. Behind the scenes, each `await` point expanded directly into a polling loop: if the inner future yielded `Pending`, the outer wrapper immediately yielded `Pending` straight up the stack back to the top-level executor.

[21:09](https://www.youtube.com/watch?v=skos4B5x7qE&t=1269s) How `.await` Desugars and the Usability Crisis of Early Futures

When awaiting a future, the execution loops: if polling returns `Pending`, the task yields and waits until a waker notification signals it to resume. Once the future finishes, it yields its value, execution breaks out of the loop, and the await expression evaluates to that final result. While this async/await compilation strategy produced genuinely zero-cost futures, releasing the early `futures` crate into the wild exposed severe usability friction. Developers quickly encountered impenetrable compiler error messages complaining that futures were not `'static` or failed to implement particular traits. Programmers found themselves mindlessly following compiler suggestions—annotating closures with `move`, cloning data repeatedly, and wrapping variables in `Arc` or `RefCell`—adding unnecessary runtime overhead and turning their codebases into unreadable "soup."

```mermaid flowchart TD accTitle: Await Loop State Machine accDescr: The poll loop of an await expression handling pending and ready states. subgraph AwaitLoop["Await Expression Desugaring"] Start["Poll Awaited Future"] --> Check{"Result?"} Check -->|"Pending"| Wait["Wait for Wake-up Notification"] Wait -->|"Woken up"| Start Check -->|"Ready(val)"| Break["Break loop with value"] end ```

Combinators exacerbated the issue by generating sprawling, terminal-filling types, such as endless chains of `AndThen<AndThen<MapErr<TcpStream...>>>`, through which developers had to dig just to locate the underlying compiler error. Boats highlights a Reddit comment that captured the community's sheer exasperation: “having to use `RefCell` or `clone` everything for each future leads to over-complicated code and it makes me wish that Rust just had garbage collection.”

- Compiling async/await down to state machines was the right theoretical foundation, but early implementations alienated users. - Combinator chains created massive types that obscured the real source of compile errors across entire terminal screens. - Programmers reacted by sprinkling `move` closures, reference counters, and clones everywhere without understanding why.

[23:08](https://www.youtube.com/watch?v=skos4B5x7qE&t=1388s) The Borrowing Problem: Why Self-Referential Structs Break Rust

Evaluating the situation around 2017–2018, the Rust team identified two distinct problems: compiler diagnostics had to be improved by embedding async/await syntax directly into the language, and a fundamental design limitation dubbed "the borrowing problem" had to be solved. In the original design of `futures`, a program could not borrow across an await point. Whenever an await occurred, no references could remain active. Because normal Rust APIs idiomatically pass references, developers constantly bounced off this limitation. Boats explained the core design obstacle plainly: “In the original design of futures you could not borrow across an await point.”

```mermaid flowchart LR accTitle: Self-Referential Struct Invalidation on Move accDescr: How moving a self-referential struct invalidates internal pointers. subgraph MemoryLocationA["Stack Location A (Before Move)"] SQL_A["sql: String"] Ref_A["query_ref: &String"] -->|"points to"| SQL_A end

subgraph MemoryLocationB["Stack Location B (After Move)"] SQL_B["sql: String"] Ref_B["query_ref: &String"] end

MemoryLocationA -->|"memcpy / move"| MemoryLocationB Ref_B -.->|"dangling pointer!"| SQL_A ```

When an async function is compiled into a state machine struct that stores its local variables across suspension points, holding a reference across `.await` turns that struct into a self-referential data structure. For example, in a `get_user` method that builds a SQL string and passes a reference to `query(&sql).await`, the future's state contains both the string and a reference pointing directly back into another field of the very same struct. If Rust moves that struct in memory (via a standard bitwise copy), the self-referential pointer does not update; it still points to the old memory address, which has now been invalidated. This creates a dangling pointer, violating Rust’s core memory-safety guarantees.

- Rust has no general-purpose language mechanism to permit self-referential structs because standard moves bitwise copy memory without updating internal pointers. - Banning references across await points forced developers into awkward workarounds like reference counting and explicit clones. - If the compiler could safely permit cross-await borrows, async code could look and feel like standard, idiomatic Rust.

[26:17](https://www.youtube.com/watch?v=skos4B5x7qE&t=1577s) Inventing `Pin`: Zero-Cost Guarantees for Immovable Data

The key insight that resolved the borrowing problem was realizing that futures do not actually need to move. In the standard async executor model, the future is boxed on the heap; the reactor and executor pass lightweight handles back and forth, while the future itself remains completely stationary. Boats emphasized the realization that unlocked the solution: “The future itself never actually moves and so it's totally fine for the future to contain self-references as long as you never move it and you don't need to move it.”

To encode this immovable guarantee safely into the type system without runtime overhead, the team introduced `Pin`. `Pin` is a pointer adapter wrapping pointer types (such as `Pin<Box<T>>` or `Pin<&mut T>`) that guarantees the referent will never be moved again in memory until it is dropped and deallocated. Crucially, safe code is prevented from extracting an unpinned mutable reference from a `Pin`; doing so requires calling an `unsafe` method. The `Future` trait was rewritten so that its `poll` method accepts `Pin<&mut Self>` instead of a plain `&mut Self`. By guaranteeing that pinned futures cannot be relocated, compilers can safely generate self-referential async state machines.

```mermaid flowchart TD accTitle: Pinning in the Future API accDescr: Boxed future pinned on the heap and polled via pinned reference. subgraph Heap["Heap Memory"] PinnedFut["Pinned Future State Machine\n(Self-Referential Struct)"] end

subgraph Execution["Executor Polling"] BoxPin["Pin<Box<Future>>"] -->|"guarantees no moves"| PinnedFut PollCall["poll(self: Pin<&mut Self>, cx)"] -->|"passes pinned ref"| PinnedFut end ```

- `Pin` acts as a zero-cost wrapper around existing pointer types, enforcing that the underlying data will stay at a stable address. - Transitioning from `&mut self` to `Pin<&mut Self>` in `Future::poll` opened the door to natural borrowing patterns across `.await`. - Async functions with cross-yield borrows can now be written just like standard blocking I/O code, compiling into state machines as efficient as hand-written implementations.

[29:41](https://www.youtube.com/watch?v=skos4B5x7qE&t=1781s) Stabilization Timeline and Future Directions: Streams and Traits

With the design finalized, Boats laid out the concrete status and immediate roadmap for async Rust in early 2019. The foundation, `Pin`, had already reached stable Rust about a month prior. The core `Future` trait was slated for stabilization in Rust 1.35 or 1.36 within the following two to three months, paving the way to stabilize high-level `async`/`await` syntax by the end of summer 2019. This rollout would finally allow developers to build high-performance, non-blocking network services using familiar, linear syntax.

Looking beyond immediate stabilization, the team was already mapping out subsequent milestones. The next major primitive on the horizon was the `Stream` trait—the asynchronous counterpart to `Iterator`. While a `Future` resolves asynchronously to a single value, a `Stream` yields multiple values over time. This makes streams essential for protocols like WebSockets, streaming HTTP, and HTTP/2 server push, where applications exchange continuous series of requests and responses rather than single RPC round-trips. Boats also flagged another major language limitation being targeted: the inability to use `async fn` inside Rust traits.

- Pinning stabilized approximately one month before the talk (Rust 1.33). - The `Future` trait was projected to stabilize around Rust 1.35–1.36, with complete `async`/`await` syntax targeted for the end of summer 2019. - Long-term language work includes stabilizing `Stream` for multi-value asynchronous iteration and solving the challenges of `async fn` in traits.

[31:11](https://www.youtube.com/watch?v=skos4B5x7qE&t=1871s) Looking Beyond: Generators, Streams, and the Core Architectural Insights

There is substantial compiler work underway to fully support asynchronous programming, and looking beyond the immediate feature set reveals even broader possibilities for the language. Specifically, there is interest in bringing general generators to Rust, functioning much like generators do in Python or JavaScript. Rather than writing traditional functions that solely return values, developers would be able to write functions that yield. These functions could then be resumed right after the yield point, providing an ergonomic mechanism for constructing iterators and asynchronous streams—directly mirroring how an `async fn` allows a developer to write code that compiles into a `Future`.

Boats brings the talk to a close by summarizing the fundamental breakthroughs that made this entire zero-cost asynchronous model viable. As Boats puts it: “Recapping the real critical insights that led to this sort of zero cost async I/O model were that first was this poll based version of futures where we hope to compile these futures into these really tight state machines and then secondly that this way of doing async await syntax where we're able to have references across await points because of pinning.” Closing out their slide presentation, Boats encounters a minor slide glitch, remarking with a laugh that the final screen was “supposed to say thank you I don't know how it became an ex,” before exiting the stage to audience applause.

```mermaid flowchart TD accTitle: Pillars of Zero-Cost Async IO in Rust accDescr: The two core architectural innovations enabling zero-cost async in Rust: poll-based futures and pinning.

subgraph Insights["Core Architectural Innovations"] A["Poll-Based Futures"] --> B["State Machines<br/>(Tight, stack-allocated, no heap overhead)"] C["Pinning System (Pin)"] --> D["Borrowing Across Await Points<br/>(Guarantees memory immovable in place)"] end

subgraph FutureLang["Language Extensions"] E["Generators (Yield / Resume)"] --> F["Ergonomic Iterators & Streams"] end

Insights --> G["Zero-Cost Async I/O Model"] ```

- **Generators in Rust**: Intended to work similarly to Python or JavaScript generators by yielding values and resuming execution, offering a natural abstraction to implement streams and iterators just as async functions implement futures. - **Poll-based Future Design**: Compiles asynchronous tasks into compact, highly optimized state machines without requiring heap allocation or callback indirection per combinator. - **Pinning Guarantee**: Ensures self-referential structures generated across await points cannot be moved in memory, unlocking safe references across asynchronous suspension points.

## Notable quotes > [26:17](https://www.youtube.com/watch?v=skos4B5x7qE&t=1577s) The future itself never actually moves and so it's totally fine for the future to contain self-references as long as you never move it and you don't need to move it. — without boats

## Mentioned without boats, Alex Crichton, Aaron Turon, Bjarne Stroustrup, Rust, Mozilla Research, RustLatam 2019, C++, C, Go, Java, JavaScript, Python, Pin, Future, Stream

## Video description Boats is a member of the language design, library, and cargo teams of the Rust Project. They are Senior Research Engineer working on Rust for Mozilla Research. Contributing to Rust since 2014, they've done design work for significant language extensions like generic associated types and constant generics. For the last year, Boats has been focused on adding async/await syntax for non-blocking I/O to Rust.

Follow us on Twitter: https://twitter.com/rustlatamconf

[Source](https://www.youtube.com/watch?v=skos4B5x7qE)

中文简报

Rust稳定化Async-Await以实现零成本网络I/O 通过将基于轮询的Future与用于保护自引用结构的新型Pin指针相结合,Rust实现了符合人体工程学的异步编程,其性能可媲美手写状态机。 在本次主题演讲中,without boats探讨了Rust如何在不牺牲底层系统控制或不需要庞大运行时的情况下实现零成本异步I/O。boats梳理了从1.0版本前废弃的绿色线程、有缺陷的基于回调的Future,到Alex Crichton和Aaron Turon提出的基于轮询的状态机模型的演进历程。演讲重点指出了跨挂起点借用时自引用结构所引发的重大可用性危机,并解释了`Pin`类型的发明如何解决了该问题。最后,boats展示了async/await在2019年的稳定化路线图,以及针对异步trait、Stream和语言级生成器的未来规划。 Rust放弃了1.0版本前的绿色线程架构,因为运行时强制的调度器会对所有二进制文件带来隐式开销,并严重破坏与C语言的无缝互操作性,违背了零成本抽象的原则。 (7:36) 与其他语言中需要大量堆分配的基于回调的Future不同,Alex Crichton和Aaron Turon提出的基于轮询的Future模型使编译器能够将异步工作流扁平化为单次分配的枚举状态机。 (13:40) 跨await点持有引用会产生自引用结构,这些结构在内存移动时会被破坏;Pin类型的引入通过在不产生运行时成本的情况下保证不可移动的内存地址,解决了该问题。 (26:17) 继Pin在Rust 1.33中稳定化后,核心Future trait和async/await语法计划于2019年年中至下半年全面稳定化,为异步Stream和通用生成器铺平了道路。 (29:41)

Reading summary

Speaking at RustLatam 2019, Mozilla research engineer without boats argued that Rust achieved genuine zero-cost asynchronous I/O by pairing an inverted, poll-based future architecture with the type-level memory pinning system. Abandoning Green Threads and Callback Futures (7:36) Rust required an asynchronous I/O architecture capable of scaling to high-concurrency network workloads where standard blocking operating system calls fall short. Early in its development, Rust used an M:N green-threading runtime similar to Go or early Java. While green threads offered lightweight userspace context switching and small initial stacks, the model required a mandatory runtime embedded in every compiled binary. This global overhead directly violated the first rule of zero-cost abstractions—that unused features must impose no penalty—and made embedding Rust libraries into legacy C architectures prohibitively difficult, leading to the complete removal of green threads prior to Rust 1.0. Subsequent attempts to build purely library-level asynchronous futures around traditional callback chains also failed. Following conventions from languages like JavaScript, early prototypes registered closures across event completions. In Rust, this design required extensive heap allocations of boxed trait objects (`Box<dyn FnOnce>`) and ubiquitous dynamic dispatch. Hand-written state machines performed substantially faster, violating the second tenet of zero-cost abstractions: that an abstraction must match the performance of hand-crafted code. Inverting Control via Poll-Based State Machines (13:40) To replace callback chains, Alex Crichton and Aaron Turon designed a poll-based model where an external executor periodically polls a future for progress instead of having the future register a completion callback. A polled future returns either `Ready(T)` or `Pending`. If an operation cannot finish, the future registers a `Waker` with an event-monitoring reactor, which notifies the executor to poll again once system I/O finishes. This inversion enabled simple task cancellation by dropping the future handle and cleanly decoupled the scheduler (executor) from the I/O event registry (reactor). Critically, the poll-based model allowed the Rust compiler to collapse an entire asynchronous workflow into an `enum`-like state machine occupying a single allocation when spawned. Each suspension point is structured as an internal union holding only the local variables needed to resume execution from that state. In early benchmarks across multiple languages, this architecture positioned Rust at the leading edge of asynchronous throughput. The Usability Crisis and Borrowing Across Await (21:09) Although poll-based futures were technically performant, early authoring patterns forced developers into chaining combinators like `.and_then()` and `.map()`. This created deeply nested closures, terminal-filling compiler types, and unreadable error messages. Developers routinely resorted to defensive workarounds, cloning data and introducing `Arc` or `RefCell` wrappers to satisfy `'static` lifetime requirements. Directly integrating straight-line `async`/`await` syntax uncovered a deeper obstacle dubbed "the borrowing problem." Idiomatic Rust frequently passes references, but the original futures design could not safely hold a borrow across an `.await` suspension point. Compiling local variables across suspension points into an internal state machine turns data holding an active borrow into a self-referential struct. If Rust were to relocate that struct in memory via a standard bitwise move, the internal reference would point to stale memory, causing a dangling pointer and breaking memory-safety guarantees. Pinning and the Asynchronous Roadmap (26:17) The resolution came from the realization that an active, heap-allocated future does not need to move once polling begins. To express this constraint in the type system without runtime overhead, Rust introduced `Pin`, a pointer wrapper (such as `Pin<Box<T>>` or `Pin<&mut T>`) that guarantees the referenced data will remain at a stable memory address until dropped. Safe code cannot extract an unpinned mutable reference from a `Pin`. By updating the core trait signature to `Future::poll(self: Pin<&mut Self>, cx)`, the compiler could safely generate self-referential state machines, allowing developers to write natural cross-yield borrows. Boats outlined a multi-stage stabilization schedule: `Pin` had reached stable Rust roughly one month prior in Rust 1.33, the core `Future` trait was slated for stabilization in Rust 1.35 or 1.36, and full `async`/`await` syntax was targeted for the end of summer 2019. Future language initiatives included standardizing the `Stream` trait for multi-value asynchronous iteration, introducing general `yield`/`resume` generators, and enabling `async fn` definitions inside traits. Rust's async design successfully preserved zero-cost abstractions by combining poll-based state machines with type-level pinning guarantees. However, Boats emphasized that significant compiler and language challenges remained unresolved at the time of the presentation, particularly standardizing multi-value streams, general generators, and supporting asynchronous functions within traits.

Source quotes

[26:17] without boats: The future itself never actually moves and so it's totally fine for the future to contain self-references as long as you never move it and you don't need to move it.

Original transcript

[0:07] hey everyone thanks for having me yeah [0:12] I'm my name is without boats as in yeah [0:16] see about this you can most people call [0:20] me boats which is actually even more [0:21] confusing I am researcher at Mozilla I [0:27] work on rust my full-time job it's [0:30] pretty cool [0:32] I think so this talk is going to be [0:38] about sort of its feature that I've been [0:40] working on for about the last year and a [0:43] half and before me other people were [0:45] working on fervor it's I mean honestly [0:48] since the very beginning of rust before [0:50] run play now but before that I just [0:54] wanted to thank the organizers for [0:56] having me and for all of their work in [0:57] putting this conference on I think I may [1:03] be someone's already said pointed this [1:04] out but I believe this is the first rust [1:07] conference outside of the United States [1:09] or Europe and so as someone who works in [1:11] rust I'm really like excited and glad to [1:13] see our global community like thriving [1:14] and growing and it's really cool to see [1:16] all the conferences that are happening [1:17] this year come on to the technical stuff [1:22] so the feature that I've been working on [1:26] is this thing called async/await it's [1:30] sort of going to be probably the the [1:32] biggest thing that we do in the language [1:34] this year we're planning to ship it [1:36] sometime in the next few months and it's [1:39] the solution to this problem that we've [1:40] been struggling with for really long [1:41] time which is how can we have a zero [1:44] cost abstraction for asynchronous i/o in [1:48] rust so I'm going to explain what zero [1:53] cost abstraction means in a moment but [1:55] first just to kind of give an overview [1:57] of the feature so async/await [2:03] it's just these two new keywords that [2:04] we're adding to the language a sink and [2:07] a weight and so a sink is this modifier [2:11] that can be applied to like function [2:14] we're now the function instead of when [2:16] you call it runs all the way through and [2:17] returns instead of returns immediately [2:19] and returns this future that will [2:21] eventually result in whatever the [2:23] function would return and inside of an [2:25] async function you can take the await [2:28] operator and apply it to other features [2:30] which will pause the function until [2:32] those features are ready and so it's [2:34] this way of handling asynchronous [2:35] concurrent operations using these [2:37] annotations that makes them much easier [2:40] to write so you're just a little code [2:43] sample just to sort of highlight and [2:46] explain the future this is in like [2:51] basically just the adapter on like a [2:54] like a kind of ORM type of thing it's [2:56] handwritten where you have this get user [2:59] method which takes a string for a [3:02] username and then returns this like user [3:04] domain object by querying the database [3:06] for the record for that user and it does [3:09] that using async IO which means that [3:11] it's an async function instead of just a [3:12] normal function and so when you call it [3:15] you can await it and then just to walk [3:18] through the the body of this this method [3:20] you just the first thing is it creates [3:22] the sequel query interpolating the user [3:26] name into you know select from users [3:28] table and then we query the database and [3:32] this is where we're actually performing [3:34] some i/o so query also returns a future [3:37] because it's doing this async i/o and so [3:40] when you query the database you just add [3:42] this await in order to wait for the [3:43] response and then once you get the [3:45] response you can parse a user out of it [3:48] this user domain object which is you [3:50] know part of your application when so [3:54] this method is just sort of like a toy [3:55] example for the talk but I wanted to [3:56] highlight in it is that the only [4:00] difference really between this and using [4:02] blocking i/o or these little annotations [4:03] where you just mark the functions as [4:05] being hey sync and when you call them [4:06] you add this away and so it's you know [4:09] relatively little overhead forgetting [4:12] using non-blocking i/o instead of [4:14] blocking i/o and in particular and rust [4:17] the really great thing about our [4:19] implementation that it makes me really [4:21] excited about it is that our async await [4:24] and futures are [4:26] zero cost abstraction and so zero cost [4:34] attractions are sort of utterly defining [4:37] feature of rust it's one of the things [4:38] that differentiates us from a lot of [4:40] other languages is that we really care [4:43] about when we add new features that they [4:46] are zero cost we didn't actually come up [4:49] with the idea it's a big thing in C++ [4:53] also and so I think the best explanation [4:54] is this quote from be honest true strep [4:56] which is that a zero cost attraction [4:58] means that when you don't use it you [5:00] don't pay for it and further when you do [5:02] use it you couldn't hand coded any [5:04] better and so there's these two aspects [5:09] to a zero cost abstraction which is that [5:11] the first is that the feature can't [5:13] effect can't add costs to people who [5:15] aren't using the feature so we can't add [5:17] this global cost that will slow down [5:18] every program and or been able to in [5:20] order to enable this feature and the [5:24] second is that when you do use the [5:28] feature it can't be slower than if you [5:31] didn't use it and then you feel like oh [5:32] well I would like to use this really [5:33] nice feature that makes it easier but it [5:35] will make my program slow and so I'll [5:36] just like write this thing by hand it'll [5:38] be a much bigger pain and so I'm gonna [5:42] kind of walk through the history of how [5:43] we try to solve async Iowa and rust and [5:45] some of the steps along the way we hit [5:48] had features that failed the zero cost [5:51] test upon both both principles so the [5:59] specific problem that we're trying to [6:01] solve is async i/o and so normally IO is [6:08] blocking so when you use IO you'll block [6:11] thread and that will stop your program [6:13] and then have to be rescheduled by the [6:15] OS and the problem with blocking i/o is [6:19] just doesn't really scale when you have [6:21] trying to serve a lot of connections [6:22] from the same program and so for these [6:25] kinds of really high scale network [6:26] services you really need some form of [6:28] non blocking or asynchronous IO and rust [6:34] in particular is supposed to be designed [6:36] for languages that have these really [6:37] high performance report [6:38] Armin's you know it's a systems foreign [6:40] language for people who really care [6:42] about the computing resources they're [6:43] using and so for us to really be [6:45] successful in the network space we [6:47] really need some sort of solution to [6:48] this asynchronous i/o problem but the [6:55] big problem with async is that the way [6:57] it works is that when you call the i/o [6:59] you know system call it just returns [7:02] immediately and then eventually you can [7:04] continue doing other work but it's your [7:06] programs responsibility for figuring out [7:08] how to like schedule calling back to the [7:13] tasks that you were at the pause on when [7:15] you're doing the asynchronous i/o and so [7:18] this makes you know writing an async on [7:19] your program much more complex than [7:21] writing one of these is blocking i/o and [7:23] so a lot of languages that are trying to [7:25] target these like scalable network [7:26] services have been trying to come up [7:28] with solutions for this problem that [7:30] like take it make it not the end users [7:32] problem but a part of the language or a [7:33] part of the libraries and so the first [7:36] solution that rust started with was the [7:39] city of green threads which have been [7:40] successful in a lot of languages and so [7:43] green threads basically looked like [7:46] blocking i/o they look like spawning [7:47] threads and then just blocking on i/o [7:50] and everything looks exactly the same as [7:52] if you were using the native OS [7:53] primitives but they've been designed as [7:55] part of the language runtime to be [7:58] optimized for this use case of having [7:59] these network services which are crying [8:02] responding you know thousands tens of [8:04] thousands maybe millions of green [8:06] threads at the same time a language I [8:10] think the right now that is like having [8:11] a lot of success with this model is go [8:13] where they're called go routines and so [8:15] it's very normal for a go program to you [8:16] have tens of thousands of these running [8:18] at the same time because they're very [8:20] cheap to spawn unlike OS threats and so [8:24] just the advantage of green threads is [8:26] that the memory overhead when you spot [8:30] an OS thread is much higher because you [8:32] create this huge stack for each OS [8:34] thread whereas green threads the way [8:36] they normally work is that you will [8:37] spawn a thread that starts with a very [8:39] small stack that can grow over time and [8:40] so spawning a bunch of new threads that [8:42] aren't using a lot of memory yet is no [8:45] much cheaper and also a problem with [8:48] using like the operating system [8:49] primitives is that you depend on the [8:51] operating system [8:52] scheduling which means that you have to [8:54] switch from your programs memory space [8:57] into the kernel space and the context [8:59] switching adds a lot of overhead if you [9:02] once you start having you know tens of [9:03] thousands of threads that are all being [9:04] skipped like switched between really [9:07] quickly and so by keeping that [9:09] scheduling in the same program you'll [9:11] avoid these contexts which is really [9:13] reduces the overhead and so green [9:15] threading is a pretty good model that [9:17] works for a lot of languages both go in [9:20] Java [9:20] I believe used to smaadahl and rust had [9:24] it for a long time but removed it [9:25] shortly before Mulino and the reason [9:28] that we moved to this that ultimately [9:29] was not a zero cost abstraction [9:32] specifically because of the the first [9:36] issue that I talked about where it was [9:39] imposing costs on people who didn't need [9:40] it so if you just wanted to write a rust [9:43] program that didn't need to use screen [9:44] prints that wasn't a network service you [9:46] still had to have this language runtime [9:47] that was responsible for scheduling all [9:49] of your green threads and so this is [9:53] especially a problem for people who were [9:54] trying to embed rust inside of like a [9:56] larger C application it's one of the [9:59] ways that we've seen a lot of success in [10:01] people adopting rust is that they have [10:03] some big C program and they want to [10:05] start using rust and so they start [10:07] integrating a bit of rust into their [10:09] probe into their program it's just [10:10] writing one section of the code in rust [10:14] but the problem is that if you have to [10:16] set up this runtime in order to call the [10:19] rust then that it's too expensive to [10:21] just have a small part of your program [10:23] in rust because you have to set up the [10:24] runtime before you can call the rust [10:25] functions and so for 1.0 we removed [10:29] green threads from the language we [10:30] removed this Lingus runtime and we now [10:31] have you know a runtime that is [10:33] essentially the same as c and so it [10:35] makes it very easy to call between rust [10:36] in c and it's very cheap which is one of [10:38] the key things that makes rust really [10:40] successful and having removed green [10:45] threads we still needed some sort of [10:47] solution to async i/o but we what we [10:50] realized was they need to be a library [10:52] based solution we needed some sort of [10:53] with providing a good abstraction for a [10:58] single you know it's not a part of the [10:59] language it was not a part of this [11:00] runtime that came with every program it [11:02] was just this library that you could opt [11:03] into and use when you needed it [11:06] the most successful library solution in [11:09] general is this concept called futures [11:11] and it's also called promises and [11:13] JavaScript so the idea of the future is [11:20] that it's this it represents a value [11:23] that may not have evaluated yet and so [11:25] you can manipulate it before you [11:27] actually have for the future it's [11:29] actually resolved eventually Rose off to [11:31] something but you can start running [11:33] things with it before it's actually [11:34] resolved and there's not a lot of like [11:37] work done on futures in a lot of [11:38] different languages and they are a great [11:41] way for supporting a lot of like [11:43] combinators and especially this [11:45] async/await syntax that makes it much [11:46] more ergonomic to like build on top of [11:49] this concurrency primitive and so [11:51] futures can represent a lot of different [11:56] things so they sync IO is kind of the [11:58] the biggest most prominent one where you [12:00] know you maybe make a network request [12:01] and you immediately get a future back [12:03] which once the network request is [12:05] finished will resolve into whatever that [12:07] never coasters are turning but you can [12:09] also represent things like timeouts [12:11] where a timeout is just a future that [12:13] will resolve once that amount of time [12:15] has passed and even things that aren't [12:17] doing in the IO or anything like that [12:19] were just CPU intensive work you can run [12:22] that alone like a thread pool and then [12:24] just get a future that you hold on to [12:25] that once the thread pool is finished [12:27] doing that work the future will resolve [12:30] the problem with futures was that the [12:34] way that they've been represented in [12:35] most languages is this callback based [12:37] approach where you have this feature and [12:40] you can schedule a callback to run once [12:43] the future resolves and so the future is [12:46] responsible for figuring out when it [12:48] resolves and then when it resolves it [12:49] runs whatever your callback was and all [12:52] these distractions are built on top of [12:53] this model and this just really didn't [12:57] work for us because there's a lot of [13:00] people experiment with it a lot and [13:02] found that I just was forcing way too [13:07] many allocations essentially every [13:08] callback that you tried to schedule had [13:10] to get its own separate like crate [13:12] object heap allocation and so there were [13:14] these allocations everywhere these [13:15] dynamic dispatches and [13:17] this like approach failed the zero cost [13:19] abstraction on the second principle [13:21] where now you were it was not affecting [13:28] people who work using it but if you did [13:30] use it it would just be way slower than [13:31] if you just had an end written something [13:33] yourself and so why would you use it [13:35] because if you wrote the thing yourself [13:36] would be much faster but always hope [13:40] it's not lost sorry I also have a bit of [13:45] a cold this really great alternative [13:51] abstraction that was pull based yes we [14:00] write this model I really want to give [14:02] credit to Alex who's here and Aaron [14:06] Turin who came up with this idea where [14:11] instead of futures scheduling a callback [14:13] there instead you pull them and so [14:15] there's this other component of the [14:17] program called an executor which is [14:18] responsible for actually running the [14:20] futures and so the executor does is it [14:22] post the future and the future either [14:24] returns pending that it's not ready yet [14:26] or once it is ready it you know returns [14:28] ready and this model has a lot of [14:33] advantages one advantage is that you can [14:36] just cancel a futures very simply [14:38] because all you do to cancel them is you [14:40] stop holding them whereas with this [14:42] callback based approach it was really [14:43] difficult once you scheduled some work [14:44] to cancel that work and have it stopped [14:49] [Music] [14:50] it also really enabled us to have this [14:53] really clean abstraction boundary [14:55] between different like parts of the [14:56] program so most sort of futures [14:58] libraries come with an event loop and [15:01] that's just the your futures are [15:03] scheduled across this event loop with [15:04] these this way of doing i/o and you [15:06] really don't have any control over it [15:07] but in rust we have this really clean [15:10] boundary between the executor which [15:12] schedules your futures the reactor which [15:14] handles all the i/o and then your actual [15:15] code and so the end user can decide [15:17] which executor they want to use which [15:20] reactor they want to use giving them the [15:21] kind of control that is really important [15:23] in a systems language but the real [15:25] advantage the most important thing about [15:26] this model is that it enabled us to have [15:28] this real [15:29] perfect zero cost a way of implementing [15:31] futures where they're each represented [15:33] is this kind of state machine and so the [15:37] way this works is that when the futures [15:38] that you code that you write gets [15:40] compiled down to it's actually compiled [15:42] into native code where this is the sign [15:44] of like state machine where it has one [15:46] variant for each pause point for each [15:49] i/o event essentially and each variant [15:52] then has the state that it needs to [15:53] resume from that I appoint and that is [15:58] represented as this essentially like an [15:59] enum where it's just one structure where [16:02] it's the variant discriminant and then a [16:04] union of all of the states that it could [16:07] possibly need and so this is an attempt [16:10] to visually represent that abstractly [16:12] where this is a this state machine has [16:16] you know you perform qio events and so [16:17] it has these different states and each [16:19] state it has this the amount of like [16:21] space it needs to store everything you [16:23] will need to restore front to that state [16:26] and the entire future is just a single [16:29] heap allocation that's that size where [16:31] you just allocate that state machine to [16:34] in to one place in the heap and it's [16:39] just no additional overhead so you don't [16:41] have these like all these boxed [16:43] callbacks and things like that you just [16:44] have this like perfect really truly zero [16:47] cost model so I feel like that is [16:52] usually a bit confusing to people so I [16:54] tried to sit to my best keynote visually [16:58] represent what's going on which is that [17:02] the so you spawn a future and that puts [17:05] the future in the heap in this one [17:07] location and then if there's a handle to [17:09] it that's you started in the executor [17:10] the executor pulls the future until [17:13] eventually the future needs to perform [17:14] some sort of i/o in which case the [17:17] feature gets handed off to the reactor [17:20] and the reactor which is handling the [17:22] i/o registers that the future is waiting [17:24] on this particular i/o event and then [17:26] eventually when that i/o event happens [17:28] the reactor will wake up the future [17:30] using the Waker argument that you passed [17:32] in when you pulled it and still waking [17:35] the future up passes it back to the [17:37] executor [17:40] and then the executor will pull it again [17:42] and it'll just go back and forth like [17:44] this until eventually the future [17:45] resolves and so then when the future [17:48] finally resolved in evaluates to its [17:50] final result the executor knows that [17:52] it's done and then it drops the handle [17:54] and drops the future and the whole thing [17:55] is finished and so it forms this sort of [17:58] cycle where you pull the future wait for [18:01] IO waken up again pull it again on and [18:04] on in a loop until eventually the whole [18:06] thing is finished and this model ended [18:10] up being quite fast this is the sort of [18:13] the benchmark that was posted in the [18:15] first post about futures where [18:18] benchmarked features against a lot of [18:22] different implementations from other [18:24] languages higher is better and features [18:28] is the one on the far left so we had [18:32] this really great zero cost abstraction [18:33] that was you know competitive with the [18:36] fastest kinds of implementations of [18:39] async IO in a lot of other languages but [18:43] of course the problem is that you don't [18:45] want to write these state machines by [18:46] hand right you have your whole entire [18:47] application state as a state machine is [18:48] like not very pleasant to write but [18:51] that's where the future abstraction is [18:52] really helpful is that we can build [18:53] these other ApS on top of it and so the [19:00] first solution that we had was this idea [19:03] of futures combinators where you can [19:04] build up the state machines but applying [19:07] all of these methods to the future sort [19:09] of similar to the way that iterator [19:10] adapters like filtering map work and so [19:14] this function it just what it does is it [19:17] requests rustling on org and then [19:19] converts that the response to a string [19:21] and so instead of returning just a [19:27] string and returns a future of a string [19:29] because it's going to be an async [19:30] function and it has these futures in the [19:33] body that it's going to be calling in [19:36] those are going to actually be the parts [19:37] that do some i/o and then they're all [19:40] sort of combined together using these [19:41] combinators like and then in map and we [19:44] built all these combinators like man [19:46] then map filter map error like all kinds [19:48] of different things [19:52] and this works it has you know some [19:55] downsides especially these like nested [19:57] callbacks which can be really difficult [19:59] to read sometimes and so because you [20:02] know it has his down sights we also [20:04] start at it tried to implement an async [20:07] await implementation and so the first [20:10] version of async await was not part of [20:11] the language instead it was this library [20:13] that provided this through like a syntax [20:15] plugin and this is doing the same thing [20:19] that the previous function did it just [20:21] fetches rustling it turns it to a string [20:23] but it does so using a single agent so [20:26] it's much more like straight-line looks [20:29] much more like the way normal blocking [20:30] i/o works we're just like in the example [20:32] that showed originally the only real [20:35] difference is the annotations and so the [20:37] async annotation you know turns this [20:38] function into a future instead of just [20:40] returning immediately and then the await [20:42] annotations are wait on these on the [20:47] futures that you actually construct [20:49] inside of the function and a weight [20:55] under this Paul model these sugars to [20:59] this sort of loop where what you do is [21:03] you just pull in a loop and over time [21:06] you get pending back you yield all the [21:07] way back up to the executor that your [21:09] pending and so then it waits until it [21:11] gets woken up again and then when [21:13] finally the future that you're awaiting [21:14] finishes it [21:16] you know finishes with the value and you [21:18] break out of the loop with the value and [21:19] that's what these await expressions [21:23] evaluate to so this seemed like a really [21:31] good solution you know you have this [21:33] async await notation which is compiling [21:37] down to these really awesome zero-cost [21:39] features and so sort of released futures [21:42] into the wild and got feedback and [21:46] that's where we ran into problems that [21:51] essentially anyone who tried to use [21:53] futures quickly ran into very confusing [21:56] error messages where we've just kind of [21:59] complained about how your feature isn't [22:00] static or it doesn't implement this [22:01] trait and it would be sort of this bath [22:04] thing you didn't really understand and [22:06] the compiler would like to make helpful [22:08] suggestions which you would just follow [22:09] until eventually compiled and C would be [22:11] like annotating closures would move and [22:13] you would put things into reference [22:15] counted pointers and clone things and [22:17] this and that and it all felt like you [22:18] were adding all this overhead to the [22:19] thing that you didn't didn't seem [22:20] necessary didn't understand why you had [22:22] to do it and also when you were down [22:24] your code ended up looking like soup and [22:25] so tons of people were bouncing off of [22:27] futures and it didn't help that the [22:28] Combinator's produce these really huge [22:31] types where your entire terminal would [22:33] be filled up with like the type of one [22:35] of your Combinator chains or just be [22:37] like you know an and then a van and then [22:38] of a map bearer of a TCP stream and so [22:40] on and you know you have to dig through [22:43] this to try to figure out what the [22:44] actual error that you encountered was [22:46] and I found this quote on reddit which I [22:50] think really beautifully sums up all of [22:53] the complaints about features oh it's [22:55] just that you know when using futures [22:56] the error messages are inscrutable [22:58] having to use ref cell or clone [23:00] everything for each feature leads to [23:02] over company to code and it makes me [23:03] wish that rust just had garbage [23:05] collection was just yeah not great [23:08] feedback so looking at the situation [23:12] maybe a year year and a half ago it was [23:16] clear that there were sort of two [23:18] problems that needed to be done to make [23:21] like needed to be solved in order to [23:22] make features more usable for people and [23:25] the first was we need a better error [23:26] messages and so the easiest way to do [23:28] that is to build the syntax into the [23:30] language and then they can hook into all [23:32] of our Diagnostics and error handle [23:33] aramis you know support so that you can [23:36] have really good error messages for [23:37] async/await but the second was that most [23:41] of these errors people are running into [23:42] were actually then bouncing off a [23:44] certain obscure problem which is called [23:46] the borrowing problem where there was [23:50] this fundamental limitations in the way [23:51] that futures was designed that made it [23:53] so something really common pattern was [23:55] not possible to express and that problem [23:58] was that you can't in the original sign [24:00] of futures you could not borrow across [24:02] an awake point so if you were to await [24:04] something you couldn't have any [24:05] references that were alive at that time [24:07] and so when people were having these [24:09] problems that they were bouncing off of [24:10] what they were actually ultimately what [24:12] they were doing was they were trying to [24:13] borrow [24:14] while they were awaiting and they [24:16] couldn't do that [24:17] and so if we could just make it so that [24:18] was allowed then most of these errors [24:20] would go away and everything be much [24:22] easier to use and you could just kind of [24:23] write normal rest code with a sink and a [24:25] late and it would all work and this [24:32] kinds of borrows three weights are [24:33] extremely common because just the [24:35] natural API surface of rust is to have [24:37] references in the API and so but the [24:42] problem with futures is that when you [24:44] actually compile the future which has to [24:46] restore all that state when you have [24:48] some references to something else it's [24:50] in the same stack frame what you end up [24:51] getting is the sort of self referential [24:53] future and so here's the some code from [24:55] the original like get user method where [24:59] we have this sequel string and then when [25:00] we call query with it we use pass a [25:03] reference to the sequel string and so [25:06] the problem here is that this reference [25:12] to the sequel string is a reference to [25:13] something else that's being stored in [25:14] the same future state and so it becomes [25:17] the sort of stuff referential struct [25:18] where you have these are the fields of [25:20] the future in theory if it were you know [25:22] a real struct you would have you know [25:24] the database handle that you were in the [25:26] self aspect of it but I would also have [25:28] the sequel string and the reference to [25:32] the sequel string which is ultimately a [25:33] reference pointing back to a field of [25:36] the same struct and some fresh potential [25:39] structs are sort of a really hard [25:40] problem that we don't have a general [25:42] solution for and the reason that we [25:45] can't allow you to have referenced same [25:47] structs though when you when you move [25:51] that struct then what happens is that we [25:54] make a new copy of the struct in the [25:55] location that you're moving it to and [25:57] when the old copy becomes invalidated [25:59] but when you make that copy the [26:01] reference that was self referential is [26:03] still pointing to the old copy and [26:06] that's becomes a dangling pointer and [26:09] it's the kind of memory issues that rust [26:11] has to prevent so we can't have suffer [26:13] official structs because if you move [26:14] them around then they become invalidated [26:17] but we made this like really frustrating [26:20] in the futures case was that we actually [26:25] don't really need to move these futures [26:26] around so if you remember the the model [26:29] where the futures in the heap and a [26:30] handle [26:30] - it is getting passed back and forth [26:31] between the reactor and the executor the [26:35] future itself never actually moves and [26:37] so it's totally fine for the future to [26:39] contain self-references as long as you [26:41] never move it and you don't need to move [26:43] it so we really needed to solve this [26:45] problem with some way to express in the [26:47] API futures that while you're pulling it [26:49] you're not allowed to move it around and [26:51] then if we just could express that [26:52] somehow then we could we could allow [26:58] these kinds of self references in the [27:00] body of the future and then we could [27:01] just have these really these references [27:04] in your racing functions and everything [27:05] would work and so we were working on [27:08] this problem and ultimately we came out [27:10] with this new API called pin and so pin [27:18] is a sort of adapt her around other [27:19] pointer types where they become this [27:21] pinned reference is there's some normal [27:23] reference and a pinned reference in [27:25] addition to whatever other guarantees it [27:26] has it guarantees that the reference [27:29] will never the value but the reference [27:31] is playing - will never be moved again [27:32] and so guarantees that it's going to [27:34] stay in the same place until eventually [27:36] it gets the allocated and so if you have [27:40] something in your API which says that it [27:41] has to come be taken by pin then you [27:43] know that it will never removed again [27:44] and you can have these kinds of self [27:45] referential structs and so we changed [27:50] the way the futures work so that now [27:52] this is just being a boxed future it's [27:54] the Box future behind a pin so we know [27:56] that wherever we boxed it up and put it [27:58] in the heap it's guaranteed now but I [28:01] part of the pin API that it will never [28:04] move again and then you know when you [28:07] pull the future instead of just passing [28:08] a normal reference to it we pass the pin [28:10] reference to it and so the future knows [28:12] that it won't that it can't be moved and [28:15] the trick here that makes this all work [28:17] is that you can only get an unpin [28:19] reference out of a pin reference in [28:21] unsafe code we make it's an unsafe [28:23] function to be able to do that and so [28:25] the API looks roughly like this where [28:27] you have pin which is just you know a [28:30] wrapper around another around a pointer [28:32] type it doesn't any runtime overhead or [28:35] anything it's just like demarcates it is [28:36] being pinned and then a pin box [28:43] be converted into a pin reference but [28:45] the only way to cover two pinned [28:46] reference into an unpin reference is to [28:48] use an unsafe function and so what we [28:55] did was then we just changed the futures [28:56] API so that we instead of taking a [28:59] normal reference it takes a pinned [29:00] reference and otherwise the API is the [29:06] same as it was before and this is [29:08] essentially the API that we're going to [29:09] going to be stabilizing and so with that [29:14] change this code from the first example [29:16] just works the way that it's written and [29:19] so you can just write code exactly the [29:20] way we would write it with blocking i/o [29:22] add these async and await annotations [29:24] and then what you get is this you know [29:28] async idea with this really awesome zero [29:29] cost abstraction where it's basically is [29:34] cheapest if you hand-wrote the state [29:35] machine yourself by hand so the [29:41] situation today [29:43] pinning was stabilized in the last [29:45] release about a month ago we're in the [29:48] process of stabilizing the future API so [29:50] probably in 1/35 maybe it will slip in [29:54] b1 36 which is know in about two or [29:57] three months basically and then we're [30:00] hoping sometime this year we'll have a [30:01] single Wade state but hopefully by the [30:04] end of summer even we're going to have [30:05] it is stabilized and so that people will [30:07] be able to write non-blocking i/o [30:11] network services using this syntax that [30:13] makes it very similar to writing with [30:15] blocking i/o looking beyond that [30:20] stabilization we're also already [30:22] starting to work on these sort of more [30:25] long-term features like streams I think [30:27] is probably the next big one where it's [30:31] an async [30:33] let's so a feature is just you know one [30:34] value but a stream is many values being [30:37] yield a synchronism being yielded is [30:39] synchronously it's essentially like an [30:40] asynchronous iterator and so you'll be [30:43] able to like loop asynchronously over a [30:44] stream and things like that and it's [30:46] very important for like a lot of use [30:49] cases where you have like you know [30:50] streaming HTTP web sockets in speed to [30:54] push request that kind of thing [30:56] instead of having a networking like our [30:58] PC model where you you know make a [31:00] network request and get a single [31:01] response you have streams of responses [31:03] and requests going back and forth [31:06] there's also today a limitation that [31:08] async offend can't be used in traits [31:11] there's a lot of work being done in the [31:13] compiler to make it so that it would be [31:14] able to support that and looking out [31:17] even beyond these features sometimes we [31:19] want to have generators where for [31:23] similar to generators in like Python or [31:24] JavaScript where instead of just having [31:27] functions to return you can also have [31:28] functions that yield and so then you can [31:30] resume them again after the yield and [31:31] you can use these straight functions as [31:33] a way of writing iterators and streams [31:35] similar to how an async function lets [31:36] you write a function it's actually a [31:38] future but I guess just reserve [31:43] recapping the real critical insights [31:48] that led to this sort of zero cost is [31:49] Takaya model where that first was this [31:51] pole based version of futures where we [31:53] hope to compile these features into [31:55] these really tight state machines and [31:57] then secondly that this way of doing [32:00] async await syntax where we're able to [32:02] have references across the weight points [32:03] because of pinning supposed to say thank [32:07] you I don't know how it became an ex [32:20] [Music] [32:21] [Applause] [32:22] [Music]