Rust Stabilizes Async-Await After Scrapping Green-Thread Runtime

Rust's Journey to Async/Await — InfoQ

InfoQ

Unlike garbage-collected languages where async/await is mere syntactic sugar, Rust required compiler-level async state machines to eliminate the borrow-checker friction and heap allocations that crippled manual futures.

AI-assisted summary of Rust's Journey to Async/Await — original source. Check the original for full context.

Source published:

Key ideas

  • Rust originally shipped with green threads and an M:N runtime, but stripped them out entirely in RFC 230 before Rust 1.0 to preserve zero-cost abstractions, predictable performance, and overhead-free C FFI. (19:34)
  • Unlike JavaScript promises, Rust's zero-cost futures are lazy and poll-based, avoiding per-step allocations by compiling state machines that an external executor like Tokio drives forward. (29:41)
  • While async/await acts primarily as syntactic sugar in garbage-collected languages, it proved critical in Rust to allow the compiler to track borrows across suspension points without forcing heap allocations. (35:36)
  • Rust adopted a distinctive postfix `.await` syntax without parentheses to prevent precedence conflicts and allow seamless left-to-right chaining with the postfix `?` error-handling operator. (40:59)

Detailed notes

## Background Published by InfoQ on YouTube on November 11, 2019, this presentation features Steve Klabnik speaking at QCon New York 2019. The talk covers the technical history and design evolution of Rust's concurrency model, detailing the challenges and trade-offs of integrating async/await as a major language feature.

## TL;DR Steve Klabnik traces the multi-year architectural evolution of asynchronous programming in Rust, from early pre-1.0 green threads inherited from its browser-engine origins to their removal in RFC 230 in favor of minimal 1:1 OS threads. He explains how manual zero-cost futures modeled after statically typed functional paradigms clashed with Rust's borrow checker, necessitating language-level async/await to construct stackless state machines without heap allocations. Klabnik details the critical breakthroughs that made stabilization possible, including lazy poll-based execution, pinning (`Pin`) for self-referential memory safety, standardizing `std::future::Future`, and resolving the contentious postfix `.await` syntax.

## Atmosphere & style The presentation carries an engaging, reflective, and pragmatic tone, blending candid engineering war stories with deep technical exposition. Klabnik speaks with self-effacing humor and clarity, openly acknowledging Rust's missteps, ecosystem friction, and painful design pivots without defensiveness. The pacing transitions smoothly from foundational concurrency theory to granular compiler and borrow-checker trade-offs, making the arduous journey toward language stabilization feel both intellectually rigorous and hard-won.

## Bulletin - [19:34](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1174s) Rust originally shipped with green threads and an M:N runtime, but stripped them out entirely in RFC 230 before Rust 1.0 to preserve zero-cost abstractions, predictable performance, and overhead-free C FFI. - [29:41](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1781s) Unlike JavaScript promises, Rust's zero-cost futures are lazy and poll-based, avoiding per-step allocations by compiling state machines that an external executor like Tokio drives forward. - [35:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2136s) While async/await acts primarily as syntactic sugar in garbage-collected languages, it proved critical in Rust to allow the compiler to track borrows across suspension points without forcing heap allocations. - [40:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2459s) Rust adopted a distinctive postfix `.await` syntax without parentheses to prevent precedence conflicts and allow seamless left-to-right chaining with the postfix `?` error-handling operator.

## Key takeaways - [01:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=88s) Parallelism, concurrency, and asynchrony are orthogonal dimensions of execution, API design, and language semantics. - [04:31](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=271s) Cooperative multitasking relies on voluntary task yields and risks starvation, while preemptive multitasking guarantees fairness at the expense of runtime complexity. - [08:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=539s) Green threads reduce initial stack footprints significantly compared to 1:1 OS threads, but incur severe latency and context-switching costs during stack resizing and foreign C calls. - [13:15](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=795s) Because Rust was originally created at Mozilla to build the Firefox client rather than massive web servers, early designs favored green threads without anticipating modern high-density I/O bottlenecks. - [19:34](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1174s) Rust stripped out its green-thread runtime prior to the 1.0 release via RFC 230 because the runtime overhead conflicted with its core mission as a zero-cost systems language. - [21:18](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1278s) Shipping Rust 1.0 with only blocking 1:1 native threads forced high-scale server implementations into the same connection-scaling constraints seen in late-1990s web servers. - [24:25](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1465s) Rust could not replicate Go or Erlang's runtime models because pervasive runtime schedulers introduce FFI thunk overhead that breaks zero-cost C interoperability. - [29:41](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1781s) Rust futures differ fundamentally from JavaScript promises by being lazy rather than eager, executing only when actively polled by an executor. - [37:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2248s) Manual future combinators generated unreadable types and fought the borrow checker, inadvertently forcing heap allocations and defeating zero-cost performance goals. - [37:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2248s) Language-level async/await allows the Rust compiler to generate state machines that safely borrow data across await points without heap allocation. - [39:35](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2375s) The introduction of the `Pin` type resolved self-referential memory safety hazards, ensuring generator state machines remain fixed in memory while awaiting. - [39:35](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2375s) Standardizing `std::future::Future` eliminated the mandatory `Error` associated type, delegating error handling to natural composition with `Result`. - [40:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2459s) The Rust language team selected postfix `.await` over prefix keywords to preserve clean left-to-right code chaining alongside the postfix `?` try operator. - [44:13](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2653s) Rust's async/await framework seamlessly bridges with browser APIs in WebAssembly via conversion helpers between Rust futures and JavaScript promises. - [48:05](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2885s) Differing concurrency paradigms—such as Go goroutines, Erlang actors, and Rust futures—represent intentional trade-offs tailored to each language's architectural constraints rather than universal solutions.

## Detailed walkthrough ### [00:03](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=3s) Introduction: Steve Klabnik, Role, and Context

Steve Klabnik opens the talk by noting a recurring pattern in his presentations at QCon: he always seems to be speaking about language features slated to arrive roughly three months in the future. This talk covers the historical path leading up to the stabilization of Rust's `async`/`await` syntax and the final steps remaining in that effort. Klabnik clarifies his position within the Rust organization and set expectations for his perspective: he serves on the Rust core team, leads the documentation team, and works at Cloudflare (though not specifically on Rust projects there).

He underscores that he was not the primary designer behind `async`/`await`, framing his role instead as an observer and chronicler: “I like think of myself as like a amateur historian of rust development I like to keep track of how the language has evolved over time and sort of like deal with these major topics so I was not directly involved in the design I just read all the comments and talked to people over years so I'm kind of giving you my like semi outside perspective on how this went down.” While hundreds of contributors worked on the feature, he highlights withoutboats as the principal driver behind its language design over the preceding years.

[01:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=88s) Disentangling Definitions: Parallelism, Concurrency, and Asynchrony

To explain how Rust arrived at its asynchronous model, Klabnik begins by clarifying the terminology of making a computer perform multiple tasks. In conversation, developers routinely conflate three distinct concepts: parallel computing, concurrent computing, and asynchronous programming. He introduces the word *task* as a generic umbrella term for any unit of computation running inside such systems—whether backed by an OS thread, an event loop handler, or another mechanism.

Parallel computing occurs when a system executes multiple tasks truly simultaneously. Because a single CPU core cannot physically perform two instructions at the exact same instant due to the constraints of physical hardware, true parallelism strictly requires multiple CPU cores or multiple CPUs. Concurrency, by contrast, gives the illusion of simultaneous progress on a single core by interleaving execution—doing a small slice of work on one task, switching to another, and returning later. The two concepts can also compose: modern systems routinely run concurrent tasks inside multiple parallel threads across distinct cores. Asynchronous programming exists on a completely separate axis from both. It refers to language features and APIs designed to write non-blocking code that can be scheduled across parallel or concurrent executors, departing from the default line-by-line sequential execution of standard imperative programming.

```mermaid flowchart TD accTitle: Concurrency vs Parallelism Execution Models accDescr: Visual comparison showing parallel tasks running on separate CPUs versus concurrent tasks interleaving on a single core.

subgraph Parallelism ["Parallelism (Multiple Cores/CPUs)"] direction LR P1["Task A (Core 1)"] --> P1_run["Continuous Execution"] P2["Task B (Core 2)"] --> P2_run["Continuous Execution"] end

subgraph Concurrency ["Concurrency (Single Core)"] direction LR C1["Task A (Part 1)"] --> C2["Task B (Part 1)"] C2 --> C3["Task A (Part 2)"] C3 --> C4["Task B (Part 2)"] end ```

- **Task**: A generic catch-all label for any computation executing within a concurrent, parallel, or asynchronous execution context. - **Parallelism**: Hardware-level simultaneous execution requiring two or more physical cores or processors. - **Concurrency**: Logical multi-tasking where computations overlap in time through context switching, even on a single execution core. - **Asynchrony**: A programming model and API design style that breaks synchronous, sequential call-and-wait execution to allow operations to yield while waiting.

[04:31](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=271s) Cooperative vs. Preemptive Multitasking

Klabnik addresses how runtime systems switch between tasks, categorizing multitasking into cooperative and preemptive models. Under cooperative multitasking, each running task explicitly chooses when to surrender execution resources, yielding control back to a scheduler so another task can run. Klabnik recalls first encountering preemptive multitasking around the era of Windows 95 and Mac OS 8. In a preemptive architecture, the underlying operating system or scheduler interrupts computations at arbitrary boundaries without requiring permission from the running code.

The choice between these models represents a classic systems trade-off. Preemption provides safety against uncooperative or malicious code: “This is like a very interesting trade-off because in a system with cooperative multitasking if you have a bad actor they can really mess everything up because what happens if they never decide to give up control of the situation then they just hog all the resources.” While preemption is essential for multi-tenant or untrusted environments, interrupting threads and managing state carries runtime overhead and architectural complexity. Conversely, if a system operates entirely within trusted code, cooperative multitasking can yield lower overhead, provided every task yields responsibly.

- **Cooperative Multitasking**: Tasks yield control voluntarily; an infinite loop or greedy task can starve the entire system. - **Preemptive Multitasking**: The operating system or runtime forcibly interrupts tasks via timer interrupts and context switches, guarding against starvation at the expense of overhead.

[05:45](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=345s) Native Threads (1:1) vs. Green Threads (M:N)

The discussion concludes with the distinction between native threads and green threads, a foundational architectural dilemma that shaped Rust's standard library and I/O model. Native threading—also called 1:1 threading—maps each language task directly to an operating system thread. The operating system handles all thread lifecycle events, priorities, and scheduling across CPU cores. Because the kernel maintains full visibility over every active process and thread on the machine, it can make balanced scheduling decisions across the entire system. Native threads are mature, stable, and conceptually straightforward, but they carry noticeable resource costs. Operating systems must allocate kernel bookkeeping structures and reserve fixed stack sizes (which default to several megabytes on Linux and Windows), putting hard limits on the number of concurrent native threads a program can spawn before exhausting virtual memory.

Green threads—often called M:N or N:M threading—decouple user tasks from kernel threads. The language runtime instantiates its own lightweight user-space tasks and schedules $M$ program tasks onto $N$ underlying OS threads. Many languages have relied on green threads because runtimes can optimize scheduling decisions for application-specific patterns and allocate tiny stacks that grow on demand. However, this decoupling introduces a significant drawback: hiding user-space task states from the operating system kernel can cause application schedulers and the OS scheduler to work at cross-purposes, fighting each other for CPU cache and execution time.

```mermaid flowchart TD accTitle: Native 1 to 1 Threading versus Green M to N Threading accDescr: Diagram illustrating 1-to-1 mapping of application tasks to OS threads versus M-to-N mapping via a language runtime scheduler.

subgraph NativeModel ["Native Threading (1:1)"] direction TB AppTask1["Task 1"] --- OSThread1["OS Thread 1"] AppTask2["Task 2"] --- OSThread2["OS Thread 2"] end

subgraph GreenModel ["Green Threading (M:N)"] direction TB subgraph UserSpace ["Language Runtime"] GTask1["Task 1"] GTask2["Task 2"] GTask3["Task 3"] Scheduler["Runtime Scheduler"] GTask1 --> Scheduler GTask2 --> Scheduler GTask3 --> Scheduler end subgraph KernelSpace ["OS Threads"] OS1["OS Thread A"] OS2["OS Thread B"] end Scheduler --> OS1 Scheduler --> OS2 end ```

- **1:1 (Native) Threading**: - Leverages OS kernel scheduling with system-wide awareness. - Traditional, well-understood API model. - High baseline resource footprint per thread due to OS bookkeeping structures and default multi-megabyte stack allocations. - **M:N (Green) Threading**: - Managed by a language runtime scheduler mapping many user tasks onto fewer OS threads. - Allows highly dense task allocation with minimal initial memory overhead. - Obscures task states from the kernel, creating potential scheduling conflicts between the runtime and the OS.

[08:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=539s) The Trade-offs of Green Threads vs. Native Threads

Green threads offer a massive operational advantage when it comes to memory footprint and density. While native operating system threads are heavy, green threads typically initialize with a call stack that is only a tenth to a hundredth of the size of an OS thread's default stack. This dramatically reduced overhead makes it trivial to spawn tens to hundreds of times more threads; in systems architected around green threads, spinning up a million concurrent threads is a completely routine design pattern. Attempting that same scale with native OS threads would rapidly exhaust physical RAM.

However, severing execution from the native operating system introduces harsh disadvantages. Because a green thread starts with a minuscule stack, running out of stack space requires runtime intervention: the runtime must resize the stack dynamically, reallocate memory, and copy the existing stack frames over to the new location. Implementations vary wildly and often introduce unexpected latency spikes. Worse, interoperability with native C code becomes fraught. C ABI expectations demand a standard, continuous native stack. Crossing the boundary between a tiny green thread stack and a native C stack requires context switching and stack swapping that incurs noticeable runtime overhead. As Steve Klabnik observes regarding the architectural friction:

> “And largely the story that I'm going to be telling you today is about how Rust thought that one of these things was the right way to do things and then painfully over years slowly switch to the other one sort of.”

- **Stack size disparity:** Green threads start with 1/10 to 1/100 the stack footprint of an OS thread. - **High-density scaling:** Green thread architectures comfortably support 1,000,000 concurrent threads without RAM exhaustion. - **Stack growth penalties:** Exceeding the initial stack limits forces the runtime to reallocate and copy stack memory. - **C FFI boundary cost:** Calling foreign C functions requires switching to a real native stack, introducing performance overhead.

---

[10:20](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=620s) The C10k Problem and the Evolution of Web Concurrency

To understand why programming languages care about concurrency models, Klabnik revisits the infamous C10k problem—the late-1990s milestone of designing web servers capable of serving 10,000 clients simultaneously. Reading directly from Dan Kegel’s historic 1999 web page, Klabnik highlights the retro computing era when a top-tier server was a 1 GHz machine equipped with 2 gigabytes of RAM and a gigabit Ethernet card for $1,200. He singles out an anecdote from the era that epitomizes the late-90s mindset: in 1999, one of the busiest FTP sites on the internet, `cdrom.com`, successfully handled 10,000 simultaneous clients.

```mermaid flowchart TD accTitle: Evolution of Web Server Concurrency Architecture accDescr: Shows the progression from Apache prefork to worker multi-processing models.

subgraph Prefork ["Early Apache (prefork)"] direction TB L1["Listening Process"] -->|"forks"| P1["Child Process 1 (1 connection)"] L1 -->|"forks"| P2["Child Process 2 (1 connection)"] end

subgraph Worker ["Apache Worker MPM"] direction TB L2["Control Process"] -->|"manages"| CP["Child Process"] CP -->|"thread pool"| T1["OS Thread 1"] CP -->|"thread pool"| T2["OS Thread 2"] CP -->|"thread pool"| T3["OS Thread 3"] end

Prefork -->|"too resource-intensive"| Worker ```

The historical progression of the Apache HTTP Server illustrates the mounting struggle against resource limits. Early Apache utilized a pre-forking architecture: a master control process listened for incoming connections and forked an entire child process for every single incoming client. Dedicating an entire operating system process with its associated memory tables and context-switching overhead to a single socket connection proved far too resource-intensive to sustain exponential web growth. To cope, Apache evolved to the worker MPM (Multi-Processing Module) model, where each child process managed a pool of 1:1 native OS threads that processed connections, achieving significantly higher connection density on the same hardware.

> “Like I just love that sentence because it really positions you in this like late 90s kind of mindset, right? Like FTP site not a thing people use anymore most of the time; CD-ROMs and cdrom.com just like really spells out 1999 for me.”

---

[13:15](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=795s) Early Rust, Firefox, and the Original M:N Green Runtime

Although Rust did not exist in 1999, these historical scaling challenges shaped its early architectural choices. Rust was initially conceived inside Mozilla to improve the Firefox web browser. A web browser is fundamentally an HTTP client, not a massive web server. While client-side networking requires fast, non-blocking I/O to download assets quickly, an individual browser instance never needs to juggle 10,000 or 1,000,000 inbound connections at once. Consequently, massive server-side I/O scalability was not an immediate, first-tier priority for Mozilla's engineering team.

```mermaid flowchart LR accTitle: Early Rust Concurrency Architecture accDescr: Architecture of Rust before 1.0 showing green tasks mapped to OS threads.

subgraph UserTasks ["Green Tasks (Erlang/Go Style)"] T1["Task 1"] T2["Task 2"] T3["Task 3"] T4["Task N..."] end

subgraph Sched ["green module (M:N Scheduler)"] MS["Runtime Work-Stealing Scheduler"] end

subgraph OSThreads ["1:1 Native Threads"] O1["OS Thread 1"] O2["OS Thread 2"] end

UserTasks --> Sched Sched --> OSThreads ```

In pre-1.0 versions of Rust, the language closely resembled Go and Erlang. The standard library included a dedicated `green` module that implemented an M:N green thread scheduler alongside synchronous, non-blocking networking primitives in `std::net`. The underlying design philosophy was simple: developers who wanted concurrent performance would simply spin up thousands of lightweight green threads and write straightforward sequential networking code.

- **Client vs. server focus:** Rust was built for Firefox, where handling millions of concurrent sockets was not the primary operational constraint. - **Erlang and Go influence:** Rust initially leaned heavily toward task-based message-passing and green threads rather than zero-cost system abstraction. - **Standard library green module:** The pre-1.0 distribution shipped an integrated green thread runtime and an M:N task scheduler directly out of the box.

---

[15:15](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=915s) Disentangling I/O Models: Synchronous vs. Asynchronous and Blocking vs. Non-blocking

The early Rust documentation presented what seemed like a paradox: networking I/O was documented as both synchronous and non-blocking. As Klabnik explains, many programmers mistakenly treat "synchronous" as synonymous with "blocking" and "asynchronous" as synonymous with "non-blocking." In reality, these are two independent orthogonal dimensions that define how code is written versus how the execution engine interacts with the operating system.

| Concurrency Dimension | Blocking I/O | Non-blocking I/O | |:--- |:--- |:--- | | **Synchronous Code** | **Classic Blocking:** The source code appears sequential, and thread execution halts completely on I/O (e.g., classic MRI Ruby, legacy interpreters). | **Synchronous Non-blocking:** The code is authored sequentially, but the underlying runtime intercepts I/O, yields the green thread, and unblocks transparently (e.g., Go, modern Ruby, early Rust). | | **Asynchronous Code** | **Nonsensical:** Code is authored with asynchronous callbacks/promises, yet intentionally halts the execution thread. No mainstream languages use this pattern. | **Asynchronous Non-blocking:** The code explicitly models asynchronous boundaries (callbacks, promises, futures), and runtime I/O does not block the thread (e.g., Node.js). |

Synchronous blocking I/O is intuitive to write, but its execution performance is low because threads spend significant time sitting idle. Synchronous non-blocking I/O (as used in Go, modern Ruby, and early Rust) offers an appealing compromise: developers write simple, linear code without callback hell or promise wiring, while the runtime engine transparently turns socket reads and writes into non-blocking operations, parking and resuming lightweight tasks behind the scenes. This model made it easy for scripting languages like Ruby to upgrade their internal runtimes for better I/O performance without requiring existing user code to change.

> “When you write Go code, you're not writing in a way that looks async, you're writing in a way that looks like it goes through step by step by step, but inside in the runtime it actually transparently does non-blocking I/O and it will pause your program there, whereas like if you look at Node.js, it's asynchronous and non-blocking.”

---

[17:45](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1065s) The Conflict with Rust's Identity as a Systems Language

Early Rust selected the synchronous non-blocking model paired with green threads because it gave users readable sequential syntax with non-blocking network performance. However, this design soon ran headfirst into Rust’s core ambitions. Early branding promoted Rust as a true systems programming language designed to run “blazingly fast, prevent almost all crashes, and eliminate data races.”

A systems language is expected to interface directly with hardware, produce lean binaries without mandatory runtimes, and embed seamlessly into existing C codebases. A mandatory M:N green thread runtime fundamentally clashed with that mission. Native operating system threads are provided directly by the OS kernel, whereas green threads require an omnipresent language runtime with scheduler threads, stack-checking logic, and complex foreign function interface (FFI) thunks. As Rust matured toward its 1.0 milestone, carrying a heavy, mandatory runtime began to undermine its promise of zero-cost abstractions.

> “See this is what the Rust website used to look like a long time ago, and it describes Rust as a systems programming language that runs blazingly fast, prevents all crashes, and eliminates data races. But when I told you before about like green threads versus system threads, like green threads are provided by your runtime and native threads are provided by your OS...”

[18:07](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1087s) The Dual-Runtime Compromise and the Fork Threats

In the early days of Rust's development, the core team confronted an existential identity crisis: what did it actually mean to build a systems programming language if developers were denied direct access to operating system threads? Blocking access to native OS threads seemed completely incoherent for a language with low-level ambitions. This friction sparked one of the earliest and most bitter battles in Rust's design history, to the point where community members actively threatened to fork the language.

```mermaid flowchart TD accTitle: Rust 1:1 vs M:N Threading Dilemma accDescr: Diagram showing how Rust attempted to support both native OS threads and green threads under a shared runtime interface.

subgraph App["Application Code"] Code["User Program"] end

subgraph Runtime["Runtime Abstraction Layer"] Interface["Shared Runtime Interface"] end

subgraph Engines["Execution Models"] Native["Native 1:1 OS Threads<br/>(Heavyweight, direct OS API)"] Green["Green M:N Threads<br/>(Bloated by bookkeeping)"] end

Code --> Interface Interface --> Native Interface --> Green ```

To quell the uprising, the team tried a diplomatic middle ground: they offered both. In subsequent releases, the standard library shipped two distinct packages—`native` for 1:1 operating system threading, and `green` for M:N user-space green threading. The vision was that programmers could write against a unified, swappable runtime abstraction interface and choose whichever model suited their workload at deployment time. On paper, having all the options appeared ideal, but in practice, trying to unify both models under a single interface collapsed under its own weight.

Despite the compromise, developer hostility persisted. Steve Klabnik recalls the drama with humor: “Someone did and then like five people favorite on github and nobody used the fork anyway but like it was a serious problem and like you know we weren't like happy with this situation and part of the reason why is so this is like an example of an RFC where we eventually decided to remove the runtime.”

[19:34](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1174s) RFC 230 and Shipping Minimal OS Threads in Rust 1.0

The technical reason the dual-runtime approach failed was the cost of abstraction. To build an interface flexible enough to bridge 1:1 native threads and M:N green threads, so much bookkeeping and overhead had to be added to green threads that they ceased being lightweight. Instead of being fast and nimble, green threads degraded into a clunky, inferior imitation of native threads, with worse performance and zero tangible benefit. Permitting both models produced an unmaintainable, overly complicated mess.

With the 1.0 release rapidly approaching, the team knew they would soon freeze the language and standard library interfaces permanently. Continuing with a flawed, half-baked concurrency model was out of the question. Through Rust's open design process, RFC 230 was drafted and accepted, officially stripping green threads and the entire runtime subsystem out of the language. Because Rust was fundamentally a systems language, OS threads were already provided by the underlying platform and worked reliably without runtime baggage.

When Rust 1.0 launched in May 2015, it shipped with a very minimal, blocking, 1:1 OS threading model and zero runtime magic. This austere design choice enabled the team to stabilize the language and hit the 1.0 release milestone. However, as Klabnik points out, while that resolved the initial controversy from 2011–2012, it was only the midpoint of an eight-year arc spanning all the way to 2019.

- Rust RFC 230 formally eradicated green threads and the runtime abstraction layer from the standard library. - Rust 1.0 shipped in May 2015 with exclusively native, synchronous, blocking OS threads. - The design decision favored zero-cost predictability and direct systems-level integration over bundled language runtimes.

[21:18](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1278s) The Network Service Dilemma: CPU-Bound vs. I/O-Bound

Rust 1.0 was a runaway success, but that success brought new pressures. Had Rust faded into obscurity, its minimal blocking thread API would have remained perfectly adequate. Instead, developers fell in love with the language and immediately wanted to expand its scope beyond client applications and Firefox components: they wanted to write high-concurrency network services in Rust. When they attempted to build servers using standard blocking OS threads, they ran headfirst into the architectural wall of late-1990s Apache.

Steve Klabnik highlights the tension between Rust's branding and its real-world server performance: “Russ is supposed to be a high-performance programming language but like the IO IO model feels like it's out of 1999 like old-school Apache like spin up all these big heavyweight threads and it definitely wasn't performing as well as people want it to be even though rust is like supposed to be a super high performance language and like how could we claim that like we're as fast as C well just not literally being as fast as C because IO is like a problem.”

The root cause was the classic architectural mismatch between CPU-bound and I/O-bound workloads:

| Workload Type | Defining Characteristic | Performance Bottleneck | Ideal Concurrency Model | |:--- |:--- |:--- |:--- | | **CPU-Bound** | Processor actively executing calculations and crunching numbers | Speed and core count of the local CPU | Multi-threaded parallelism across hardware cores | | **I/O-Bound** | Waiting on network roundtrips, disk responses, or remote endpoints | Latency of external systems and network sockets | Asynchronous event-driven I/O or green routines |

In an I/O-bound service, a machine spends the vast majority of its time doing nothing while waiting on remote peers. Because “the fastest thing to do is nothing at all,” an I/O system scales by letting connections idle without dedicating a costly operating system thread to each one. Tying up full OS threads—along with their associated memory stacks and scheduling overhead—just to wait on idle network sockets crippled Rust's throughput.

[24:25](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1465s) Why Rust Could Not Adopt Go or Erlang's Green-Thread Model

Languages like Erlang and Go solved the I/O-bound scaling problem by implementing asynchronous I/O under the hood while exposing a clean, synchronous-looking programming model backed by lightweight green threads (or goroutines). When an I/O call is initiated, the language runtime suspends the goroutine and yields the underlying OS thread to other work. While this is a battle-tested and effective pattern, it inherently relies on a built-in runtime scheduler.

```mermaid flowchart LR accTitle: C FFI Overhead in Green Thread Runtimes accDescr: Flow diagram showing how entering C code forces runtime stack switching or thread-parking in Go/Erlang vs zero-cost calls in Rust.

subgraph GreenRuntimes["Go / Erlang Runtime Model"] direction TB G1["Lightweight Routine"] --> Sw["Context / Stack Switch Overhead"] Sw --> CF1["Foreign C / OS Call"] end

subgraph RustNative["Rust Zero-Cost Model"] direction TB R1["Native Rust Frame"] -->|"Zero Overhead"| CF2["Foreign C / OS Call"] end ```

For Rust, the fatal flaw of green threads remained foreign function interface (FFI) overhead. Whenever an M:N green thread calls into foreign C code or native OS routines, the runtime must perform expensive stack-switching gymnastics or pin execution to a dedicated OS thread. That trade-off is acceptable for Go (which prefers rewriting everything in Go) or Erlang (which is rarely used for raw number crunching). But for Rust, zero-cost C interoperability was a non-negotiable founding principle.

As Klabnik explains: “And our constraints are that we had no overhead and calling into C we designed the language itself around this kind of thing so if we had to do a FRA oh that would be terrible and so this is also like part of why we got rid of the green green threading API that existed a long time ago too this is like a second reason that didn't work but also sort of why we couldn't really return to it.”

[25:57](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1557s) The Alternative Path: Nginx, Event Loops, and Ryan Dahl's Node.js

Since Rust could not return to green threads without abandoning its core zero-overhead guarantee, it had to look elsewhere for scalable I/O. The alternative architectural blueprint came from high-performance web servers like Nginx. Rather than spinning up an individual thread per connection, Nginx used evented asynchronous I/O managed by an event loop.

In an evented model, the server operates on an event loop—often driven by a single thread. The program registers interest in specific I/O events (such as a socket becoming readable or writable) alongside corresponding handlers, and the loop fires those handlers only when the events actually trigger. Because idle connections remain completely passive, an event loop can juggle tens of thousands of concurrent network connections without allocating heavy operating system thread stacks.

This event-loop design vaulted into mainstream software engineering consciousness in 2009 when Ryan Dahl gave his landmark presentation introducing Node.js at the first JSConf EU. Showing a grainy screenshot taken from the original hand-cam recording of that talk, Klabnik illustrates how Node.js popularized non-blocking event-driven I/O for network programming, pointing the way toward how modern languages could tackle massive concurrency without thread-per-connection bloat.

[26:57](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1617s) Node.js, Non-Blocking I/O, and Callback Hell

```mermaid flowchart LR accTitle: Evolution from Callbacks to Chained Promises accDescr: Shows the structural shift from nested asynchronous callback chains that drift rightward to linear method chaining with promises.

subgraph CB["Callback Pattern"] direction TB C1["readFile(...)"] --> C2["Callback 1"] C2 --> C3["Nested Callback 2"] C3 --> C4["Nested Callback 3 ('Rightward Drift')"] end

subgraph PR["Promise Pattern"] direction TB P1["createPromise()"] --> P2[".then(...)"] P2 --> P3[".then(...)"] P3 --> P4["Vertical Method Chaining"] end

CB -->|"Refactored into"| PR ```

Steve Klabnik opens by apologizing for a degraded, low-resolution screenshot taken from a video on YouTube, joking that when an AV setup is mediocre, you put it on YouTube, screenshot it, and paste it into a slide deck, “you get this.” The slide shows the fundamental definition of Node.js: server-side JavaScript built on Google's V8 engine with evented, non-blocking I/O. Ryan Dahl cared deeply about handling I/O efficiently, and making that possible was the very reason Node existed.

However, event-driven architectures demand non-blocking APIs. In Node, a blocking call like `fs.readFileSync` stops execution until the file is loaded into memory. To run non-blocking operations against an event loop, programs must register event listeners using callbacks. While passing a single callback closure to `fs.readFile` looks completely innocuous in simple snippets, real-world software quickly descends into "callback hell." Referencing the site `callbackhell.com` and an iconic video game meme, Klabnik notes:

> “Somebody made Ryu giving a Hadouken like to like click this thing to the pushing everything over, I don't know if anyone plays video games here, but the point is is that like when you start writing this callback stuff, stuff start drifting right and you can't really like do a whole lot about it.”

To escape this awkward horizontal drift, the JavaScript community adopted Promises. By definition, a Promise represents an eventual value from an asynchronous computation that begins in a `pending` state and resolves to either `fulfilled` or `rejected`. Rather than nesting callbacks deeper and deeper, developers can chain `.then()` handlers linearly:

```javascript myPromise.then(/* step 1 */).then(/* step 2 */); ```

While chaining replaces a deep rightward drift with a vertical chain of closures, repeatedly chaining endless `.then()` invocations remains clumsy and awkward to maintain.

[29:41](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1781s) Scala’s Futures and Rust's 0.1 Zero-Cost Implementation

```mermaid flowchart TD accTitle: Rust Future Execution Model accDescr: Contrast between eager JavaScript promises and Rust's lazy, compiler-optimized zero-cost futures executed via an executor.

subgraph JS["JavaScript Promises (Eager)"] J1["Create Promise"] --> J2["Runtime Starts Execution Immediately"] J2 --> J3["Heap allocation per chained step"] end

subgraph RS["Rust Futures 0.1 (Lazy & Zero-Cost)"] R1["Chain Futures (.and_then)"] --> R2["Compile to Single Exact State Machine"] R2 --> R3["Pass task to Executor (e.g., Tokio)"] R3 --> R4["Executor calls poll() until ready"] end ```

Simultaneously, Twitter faced severe scalability bottlenecks after building their initial infrastructure on Ruby on Rails. To resolve their I/O constraints, Twitter migrated to the JVM, embraced Scala, and published a landmark paper titled *Your Server as a Function*. The paper detailed their high-performance network services using an abstraction known as a "Future." Observing that Scala was statically typed just like Rust, the Rust team realized that futures fit their language model far better than nested callbacks or dynamic promise designs.

In August 2016, Rust announced its zero-cost futures library. In `futures 0.1`, the core abstraction was the `Future` trait—effectively an interface requiring two associated types (`Item` and `Error`) and a primary polling method:

```rust trait Future { type Item; type Error;

fn poll(&mut self) -> Poll<Self::Item, Self::Error>; } ```

Combinators like `.and_then()` allowed chaining asynchronous actions sequentially, mirroring JavaScript promises under a slightly different name.

The fundamental architectural divergence between JavaScript promises and Rust futures boils down to runtime dependencies and resource allocation:

- **JavaScript Promises are Eager:** Built directly into the language runtime, a Promise begins executing the instant it is constructed. Because tasks execute before the full chain is necessarily assembled, every chained link requires its own memory allocation. - **Rust Futures are Lazy:** Rust cannot ship a mandatory heavy runtime because it targets low-level systems workloads alongside C. A Rust future does nothing when defined; it represents an idle computation that only progresses when explicitly driven by an executor via the `poll` method. - **Single Static Allocation:** Because the compiler sees the entire chain of futures upfront, it calculates the exact memory size needed for the task at compile time. It compiles down to an optimal hand-written state machine requiring only a single allocation (or zero dynamic allocations in restricted environments) without needing resizable green-thread stacks.

An example application combining `futures 0.1` and Tokio demonstrated this paradigm: creating a TCP listener with `listener.incoming()`, chaining error handlers and combinators onto incoming sockets, and then passing that aggregate task graph into the runtime runner via `tokio::run(server)`.

- Twitter's paper *Your Server as a Function* outlined their transition from Ruby on Rails to Scala on the JVM. - Rust's zero-cost futures were formally unveiled in August 2016. - Tokio emerged as the primary asynchronous runtime executor to drive these poll-based state machines.

[34:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2076s) The Context Problem and the Futures 0.2 Transition

As developers built substantial real-world applications atop `futures 0.1`, notable architectural flaws surfaced. A major design issue was its hidden reliance on thread-local storage (TLS) for internal task bookkeeping and waking notifications. This TLS requirement meant `futures 0.1` could not function in environments lacking OS threads, such as embedded hardware and bare-metal targets.

To address this, `futures 0.2` altered the polling contract by making task state explicit through an added parameter:

```rust fn poll(&mut self, cx: &mut task::Context) -> Poll<Self::Item, Self::Error>; ```

While passing explicit context resolved the bare-metal roadblock, the release created serious ecosystem friction. Recognizing that `0.2` was merely an incremental snapshot on the way to a more comprehensive overhaul, the team signaled that `0.3` would quickly follow. When ecosystem maintainers asked whether they should upgrade their crates from `0.1` to `0.2` or wait for `0.3`, Aaron Turon published a blog post advising people not to migrate the whole ecosystem over to `0.2`.

This recommendation placed developers in an impossible paradox: libraries needed to update for users to actually test the new design, but updating to an intermediate version that would be deprecated within months seemed futile:

> “Aaron, who wrote this blog post, said you shouldn't—you shouldn't move everything over. Two is a good enough snapshot, but we're gonna have three coming out in a couple of months or less. And then people are like, that seems a little bad, cuz like you need to try it out in order to use it, but your libraries update, how are you supposed to try it out? And then if it's gonna change again, why would anyone try it out in the first place? This person was smart.”

[35:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2136s) Async/Await: From Convenience in Managed Languages to Necessity in Rust

Steve Klabnik looks back to the optimistic timeline Rust set a year prior—promising async/await in just a "couple months"—and contrasts how asynchronous execution developed across languages. Returning to JavaScript as an illustration, code evolved from nested callback hell through promise chains that mitigated rightward drift, and finally to `async`/`await`. While C# actually originated the `async`/`await` concept, JavaScript adopted it wholesale to eliminate the pain of managing millions of promises manually. In these garbage-collected languages, the feature functions essentially as an ergonomic convenience: it takes code that performs non-blocking work and lets developers write it sequentially so that it reads like traditional blocking logic.

In Rust, however, the role of `async`/`await` turned out to be far more profound. Managed languages possess runtimes and garbage collectors that easily keep track of intermediate allocations scattered across callback closures and promise chains. When Rust attempted to use manual future chaining in the `futures 0.2` era, the absence of a garbage collector turned that pattern into an agonizing battle with lifetimes. Developers had to manually track ownership across separate future stages and chains, creating friction that Rust's type system could not smoothly navigate. As Steve explains: “And here's the thing that's interesting: we didn't actually realize until we played with `futures 0.2` even more, but async/await is actually more significant to Rust than in other languages.”

- C# originally invented the `async`/`await` pattern, which JavaScript later adopted to simplify promise chaining. - In managed runtimes, `async`/`await` is primarily syntactic sugar over promises or tasks because the garbage collector handles intermediate memory. - In Rust, manual futures forced developers to grapple directly with ownership, lifetime tracking, and memory allocations across asynchronous boundaries.

[37:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2248s) The Compiler vs. Manual Futures: Lifetimes and Borrow Checking

To demonstrate why manual futures fell short, Steve shows a standard synchronous `read` method that takes `&mut self` and a buffer, filling the cursor in 1,024-byte chunks and returning the count of bytes read or an error. When attempting to translate that clean, straightforward routine into a pure futures-based implementation, the signature and associated plumbing ballooned so dramatically that the code could not physically fit onto a presentation slide. The root cause lay in the separation between creation and execution: a future is constructed up front, but handed off to an executor to run later. The Rust borrow checker was forced to apply the most restrictive rules imaginable because it could not track how memory was being passed and borrowed across those disjoint temporal steps.

This friction created a painful paradox reminiscent of Rust's early struggle with green threads versus native threads. To satisfy the compiler's strict borrow checker without compiler-level support for suspension points, developers had to resort to extra heap allocations, reference counting, and runtime checking. The zero-cost, hyper-efficient theoretical design degraded into something both slow and brutally painful to write. The arrival of language-level `async`/`await` solved this dilemma by making the compiler and borrow checker aware of the state machine's internal lifecycle. By wrapping the logic in an `async` block and using `await`, the code shrank right back down to looking nearly identical to the synchronous version while preserving memory safety without runtime overhead. As Steve put it: “It turned out the code you had to write to use the hyper-efficient thing was very inefficient, so we're now back to a place where like this is not fast and it's hard to write, so life is terrible—but async/await to the rescue.”

```mermaid flowchart TD accTitle: Synchronous versus Manual Futures versus Async-Await accDescr: Flow diagram contrasting the ergonomic synchronous approach, the broken ergonomics of manual futures, and the resolution provided by async-await.

subgraph SyncFlow["Synchronous Approach"] S1["Clean API signature"] --> S2["Simple sequential loop"] S2 --> S3["Direct stack borrows"] end

subgraph ManualFutures["Manual Futures Chaining"] F1["Massive type signatures"] --> F2["Future constructed first, executed later"] F2 --> F3["Borrow checker rejects lifetime overlap"] F3 --> F4["Forced allocations and runtime checks"] end

subgraph AsyncAwaitFlow["Language-Level Async/Await"] A1["Sequential code in async block"] --> A2["Compiler compiles state machine"] A2 --> A3["Borrow checker understands suspension points"] end

ManualFutures -->|"Solved by"| AsyncAwaitFlow ```

- Synchronous I/O in Rust uses clean signatures like `read(&mut self, buf: &mut [u8]) -> Result<usize>`. - Manual future chaining failed because the borrow checker could not reconcile borrows held across points where futures are instantiated versus when executors poll them. - To satisfy lifetime errors in pure manual futures, programmers were forced to introduce extra box allocations and runtime checks, undermining Rust's zero-cost guarantees. - Integrated `async`/`await` syntax teaches the compiler how to track borrows across yield points without heap-allocating every frame.

[39:35](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2375s) Redesigning the Future Trait: Infallibility and Pinning

Even after addressing memory ergonomics with `async`/`await`, the `Future` trait itself required a major architectural overhaul before it could graduate into the standard library as `std::future::Future`. In early revisions, futures were conceived strictly around I/O, meaning the trait definition baked an associated `Error` type directly into its interface. However, as asynchronous patterns permeated real-world software, developers found that futures infected entire codebases, including many operations that could never fail. Forcing infallible tasks to define dummy error types produced immense boilerplate for simple logic. The updated trait simplified this by dropping the dedicated error type in favor of a single `Output` type; if an operation can fail, it simply returns a standard `Result<T, E>` as its output.

The second major change to the trait signature was the introduction of `Pin`. Steve intentionally avoids diving deep into `Pin`, joking that it deserves its own dedicated 45-minute presentation. In short, `Pin` ensures that self-referential data structures generated by the compiler's `async`/`await` transformation cannot be moved in memory, which is what fundamentally allows the borrow checker to safely manage references across suspension points. Most end users will never need to interact with it directly, as it primarily concerns low-level library and runtime authors. Describing the decision to keep the core trait minimal, Steve observed: “And then finally, if you need a future that has an error, you just compose our regular old `Result` type as the `Output` type, and so futures that don't have errors are fine, futures that do have errors still work, life is good, right?”

- The original `Future` trait assumed all futures performed I/O and thus mandated an associated `Error` type. - Baking errors into the trait caused boilerplate because many internal asynchronous computations are infallible. - The standardized `std::future::Future` trait contains only a single `type Output`, allowing fallible futures to yield `Result<T, E>` naturally. - `Pin` was added to `poll(self: Pin<&mut Self>, cx: &mut Context<'_>)` to prevent memory moves of self-referential generator frames, teaching the borrow checker how to validate asynchronous borrows.

[40:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2459s) The Great Syntax War: Postfix `.await` vs. Prefix `await`

The question of which syntax to adopt for awaiting a future ignited one of the fiercest bikeshedding debates in Rust's history. Other languages like JavaScript and C# employ prefix notation, writing `await future`. However, Rust features the postfix try operator (`?`) for concise error propagation. Because asynchronous operations routinely produce errors, developers need to both await a future and bubble up its potential failure. Under a prefix `await`, writing `await foo?` created an ambiguous precedence problem: did it mean `(await foo)?` or `await (foo?)`? Almost every practical use case required resolving the future first before checking the `Result`, meaning developers would either have to wrap expressions in noisy parentheses like `(await foo)?` or the language would have to introduce confusing operator-precedence exceptions specifically for `await`.

```mermaid flowchart LR accTitle: Precedence Ambiguity in Await Syntax accDescr: Diagram contrasting how prefix await interacts awkwardly with the question mark operator versus the linear evaluation of postfix dot await.

subgraph PrefixIssue["Prefix: await expr?"] P1["await foo?"] --> P2{"Ambiguity"} P2 -->|"Intuitive parse"| P3["await (foo?)"] P2 -->|"Desired logic"| P4["(await foo)?"] end

subgraph PostfixSolution["Postfix: expr.await?"] S1["foo.await?"] --> S2["1. foo.await resolves future"] S2 --> S3["2.? unwraps Result"] end ```

The community erupted into exhaustive debates across Discourse, Reddit, and issue trackers, proposing everything from special inverted question marks to custom keywords. A screenshot of one maintainer's email client showed dozens of threads overflowing with hundreds of unread messages. Every conceivable permutation had vocal detractors, and no option achieved consensus. Eventually, the language team selected postfix `.await`—a keyword-based dot syntax with no parentheses (`future.await` or `future.await?`). While some community members viscerally disliked the departure from mainstream syntax, it preserved clean left-to-right method chaining and seamless pairing with `?`. Steve noted that while it was not his personal favorite either, moving the language forward was what mattered most: “And so at some point we decided like, hey, we can't let this incredibly important feature to Rust just be stuck down in the syntax decisions, and so we picked one.”

- Prefix syntax (`await expr?`) suffered from operator precedence conflict with Rust's postfix error operator `?`. - Chaining multiple prefix calls in JavaScript and C# often requires nesting or outer parentheses, which clashed with Rust's chained method-call conventions. - Community discussions exploded into massive arguments, logging hundreds of replies across Discourse threads, Reddit posts, and email chains. - The language team finalized postfix `.await` without parentheses, treating `await` as a reserved postfix keyword that supports left-to-right evaluation like `fetch().await?`. - Even though opinions were sharply polarized initially, developers typically adapted to postfix `.await` within fifteen minutes of practical usage.

[44:13](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2653s) Ergonomics: Runtime Macros and Async/Await in WebAssembly

Beyond the core syntax debates, the Rust ecosystem has continued developing ergonomics around async/await to make code feel natural and approachable. Steve Klabnik showcases an example using a crate named `runtime`. In a UDP echo example, the main function is decorated with a `#[runtime::main]` attribute macro. While Rust requires submitting futures to an executor, this macro rewrites the underlying code automatically, making it resemble Node.js where executor submission is handled behind the scenes. Without explicit syntax highlighting on the slide it appears somewhat dense, but inside an editor, it provides a clean, familiar structure.

```mermaid flowchart LR accTitle: JS Promise and Rust Future conversion in WebAssembly accDescr: Diagram tracing a database call returning a JS Promise into a Rust Future and back into a JS Promise. subgraph Browser ["JavaScript Runtime"] A["JS Database Call"] -->|"returns"| B["JS Promise"] F["JS Promise Output"] end subgraph RustWasm ["Rust WebAssembly Module"] C["JsFuture::from"] -->|"converts"| D["Rust Future (.await)"] D -->|"future_to_promise"| E["Converted Future"] end B --> C E --> F ```

Klabnik then highlights a wilder capability: using async/await directly inside WebAssembly (WASM). He walks through a real production example running on his own website that fetches data from a database. Because the underlying database access utilizes a JavaScript API, calling `data.get()` returns a JavaScript promise. In Rust, this is wrapped using `JsFuture::from()`, yielding a specialized Rust future that bridges the JavaScript promise so it can be awaited with `.await`. Once the data is processed, `future_to_promise` converts the Rust future back into a native JavaScript promise that is exported back to the calling environment. As Klabnik humorously describes the setup: “So yes this code and wasm is a JavaScript promise wrapped up in a rasp rust future wrapped up in a JavaScript promise and then returned through rusts back into webassembly back into JavaScript and this works it's totally fine I mean depending your definition of fine I guess but like it is a thing that if you go to my website right now this code will execute and it's a thing.”

- The `#[runtime::main]` macro hides explicit executor bootstrapping and task spawning, mirroring the effortless feel of Node.js runtimes. - `JsFuture::from` allows Rust code compiling to WebAssembly to await native browser or JavaScript promises directly. - `future_to_promise` serializes completed Rust async results back into JavaScript promises across the WASM boundary.

[46:24](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2784s) Stabilization Timeline and TechEmpower Benchmarks

Reflecting on the journey across various concurrency models, Klabnik summarizes how the team examined nearly every design space in existence before arriving at zero-cost futures. While his original conference slide predicted stabilization in Rust 1.37 or 1.38, he shares breaking news from that very morning: Withoutboats tweeted that async/await is officially being stabilized, guaranteeing its arrival in Rust 1.38, set for release around August 15. Nearly a decade after work began around 2010–2011, Rust finally established its comprehensive, zero-cost I/O model.

To illustrate the performance payoff of these design efforts, Klabnik references a recent run from the TechEmpower benchmarks. Actix, a Rust web framework, dominates the charts: the nearest competing framework from another language reaches only 62% of Actix's throughput. By refusing to compromise on runtime overhead or memory allocations, the community achieved industry-leading speeds alongside ergonomic syntax.

[48:05](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2885s) Key Takeaways: Time, Architecture, and Language Constraints

In his closing moments, Klabnik distills two fundamental lessons from the decade-long evolution of Rust's async story. First, building an efficient, production-grade I/O system cannot be rushed—it takes years of iteration, profiling, and redesign. Second, programming languages operate under vastly different design constraints, making direct comparisons between paradigms like Go's goroutines, Erlang's actor model, and Rust's poll-based futures unhelpful without context.

Klabnik emphasizes that architectural choices must respect a language's specific priorities: “Different languages have different constraints this means that you can't really argue that like go or Erlang style is better or worse than Russ style they have different pros and cons in all directions and so like just because you like a thing in one language and you move to a different language does not mean that the other language did a different way for a wrong reason.” Rust could not simply copy existing solutions because its strict non-negotiable guarantees—zero runtime overhead, no mandatory garbage collector, and cross-platform systems integration—required an entirely original path.

- Building a world-class I/O system is an iterative engineering process spanning many years. - Concurrency paradigms from Erlang or Go are neither inherently superior nor inferior; they balance different engineering trade-offs. - Rust's constraints required developing custom zero-cost abstractions rather than borrowing existing runtime models.

## Notable quotes > [35:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2136s) async/await is actually more significant to Rust than in other languages. — Steve Klabnik

## Mentioned Aaron Turon, Actix, Apache HTTP Server, C#, cdrom.com, Cloudflare, Dan Kegel, Erlang, Firefox, Go, Google V8, JSConf EU 2009, Mozilla, Nginx, Node.js, QCon New York 2019, RFC 230, Rust, Ryan Dahl, Ryu, Scala, Steve Klabnik, TechEmpower, Tokio, Twitter, WebAssembly, Withoutboats, Your Server as a Function

## Video description Video with transcript included: http://bit.ly/2XcjHbj

Steve Klabnik gives an overview of Rust’s history, diving into the technical details of how the design has changed, and talks about the difficulties of adding a major new feature to a programming language.

This presentation was recorded at QCon New York 2019: http://bit.ly/2KFk7SO

The next QCon is QCon London 2020 – March 2-4, 2020: http://bit.ly/2VfRldq

For more awesome presentations on innovator and early adopter topics check InfoQ’s selection of talks from conferences worldwide https://bit.ly/2tm9loz

#Rust #Async #ProgrammingLanguage

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

中文简报

Rust 废除绿线程运行时后终实现 Async-Await 稳定化 与 async/await 仅作为语法糖的带垃圾回收语言不同,Rust 需要编译器级别的异步状态机,以消除此前严重制约手动 Future 的借用检查冲突和堆分配。 Steve Klabnik 回顾了 Rust 异步编程历经数年的架构演进:从源自浏览器引擎背景、早在 1.0 之前就存在的早期绿线程,到 RFC 230 中将其移除并转而采用极简的 1:1 操作系统线程。他解释了借鉴静态类型函数式范式构建的手动零成本 Future 为何会与 Rust 的借用检查器发生冲突,从而迫使语言在编译器层面引入 async/await,以在无需堆分配的情况下构建无栈状态机。Klabnik 详细阐述了促成其稳定化的关键突破,包括基于轮询的惰性执行模型、用于确保自引用内存安全的固定机制(`Pin`)、`std::future::Future` 的标准化,以及解决备受争议的后缀式 `.await` 语法。 Rust 最初自带绿线程和 M:N 运行时,但在 Rust 1.0 之前的 RFC 230 中将其彻底移除,以保持零成本抽象、可预测的性能以及无开销的 C FFI 互操作。 (19:34) 与 JavaScript 的 Promise 不同,Rust 的零成本 Future 是惰性且基于轮询驱动的,通过将其编译为由 Tokio 等外部执行器推进的状态机,避免了每一步操作带来的内存分配。 (29:41) 虽然 async/await 在带垃圾回收的语言中主要充当语法糖,但在 Rust 中它至关重要,使编译器能够跨挂起点跟踪借用状态,而无需强制进行堆分配。 (35:36) Rust 采用了不带括号的独特后缀式 `.await` 语法,以避免优先级冲突,并实现与后缀式 `?` 错误处理运算符流畅的自左向右链式调用。 (40:59)

Detailed notes — zh

## Background 本内容由技术媒体频道 InfoQ 于 2019 年 11 月 11 日在 YouTube 平台发布,记录了前 Rust 核心团队成员 Steve Klabnik 在 QCon New York 2019 上的演讲。视频简介指出,Steve 梳理了 Rust 的发展历史与技术细节演进,并深入探讨了为一门系统级编程语言引入 async/await 等重大核心特性的重重挑战。 ## TL;DR 演讲系统回顾了 Rust 历经近十年、从最初的 M:N 绿色线程彻底推倒重建为现代 async/await 的曲折演进历程。为了兼顾无垃圾回收(GC)、零成本抽象以及对 C-ABI 和裸机环境的极致控制,Rust 放弃了 Go 式的厚重运行时协程,探索出基于惰性轮询与单次内存分配的状态机模型。针对手写 Future 带来的所有权与生命周期困境,Rust 引入了 Pin 机制并重构了标准库 Future。在历经关于后缀语法 .await 的社区大论战后,这套机制最终在 Rust 1.38 稳定落地,并在 TechEmpower 性能评测中展现出断层式的吞吐优势。 ## Atmosphere & style Steve Klabnik 的演讲风格轻松幽默且带有强烈的历史亲历感,他以自嘲式的“业余历史学家”口吻拆解了复杂晦涩的系统底层抽象。整场分享节奏明快、生动形象,不仅借用《街头霸王》波动拳等极客梗图调侃代码恶疾,还坦诚回顾了社区关于语法设计的白热化争吵。从最初架构方案的屡屡碰壁,到最终依靠精密设计在基准测试中登顶,演讲洋溢着硬核工程攻关后的如释重负与由衷自豪。 ## Bulletin - [18:07](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1087s) Rust 经历了从早期类 Go/Erlang 的 M:N 绿色线程到 1.0 版本全面切回 1:1 系统原生线程的重大阵痛,最终在 RFC 230 中彻底剔除内置运行时,以坚守系统级编程语言零成本抽象的定位。 - [31:38](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1898s) 与 JavaScript 的饥渴执行及动态堆分配不同,Rust 的 Future 采用完全惰性(Lazy)的轮询模型,编译期即可确定状态机内存大小,无需运行时垃圾回收即可实现极高的并发与裸机兼容。 - [40:55](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2455s) 为了协调后缀错误传播符 `?` 与链式调用,Rust 社区在激烈争议后敲定了独特的 `.await` 后缀关键字语法,消除了前缀语法带来的优先级歧义与多层嵌套括号。 - [46:24](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2784s) 历时近九年探索,Rust 的 async/await 机制确定于 2019 年 8 月的 Rust 1.38 中稳定落地,在 TechEmpower Web 基准测试中,基于该体系的 Actix 框架性能领先第二名近 40%。 ## Key takeaways - [01:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=88s) 并发是假装拥有多个核心并在微观上分时交替推进,并行则严格依赖物理多核同时执行,而异步是让代码能够以非阻塞方式并发或并行组织的抽象机制。 - [08:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=539s) 原生系统线程(1:1)与绿色线程(M:N)存在核心权衡:原生线程受制于操作系统调用栈开销难以支持海量并发,而绿色线程虽轻量却面临栈扩容拷贝及昂贵的 C 语言 FFI 互操作成本。 - [13:15](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=795s) 早期 Rust 为强化 Firefox 浏览器客户端而设计,曾内置 green 模块提供 M:N 调度与同步非阻塞 I/O,架构风格一度极度接近 Go 语言。 - [18:07](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1087s) 为了兼顾原生线程与绿色线程,Rust 曾尝试提供统一运行时抽象接口,但双重适配引入的沉重簿记开销抵消了轻量优势,迫使 Rust 团队在 1.0 前通过 RFC 230 彻底移除绿色线程。 - [21:18](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1278s) 1.0 发布后原生阻塞线程在应对海量闲置连接的 I/O 密集型任务时暴露出严重瓶颈,而 Rust 系统级语言的定位又决定了它不能容忍 Go 风格的跨 C 调用损耗,必须探索基于事件驱动的零成本方案。 - [29:41](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1781s) 借鉴 Twitter 论文《Your Server as a Function》的 Scala 实践,Rust 社区于 2016 年推出了基于状态机组合子的 futures 0.1,尝试以 Trait 和 poll 机制表达零成本 Future。 - [31:38](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1898s) JavaScript Promise 是一经创建即在后台执行的饥渴模型,每一步需动态堆分配;Rust Future 则是完全惰性的,只有在 Executor 主动调用 poll 时推进,可在编译期静态计算出整条任务状态机所需的内存大小。 - [34:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2076s) futures 0.1 深度绑定线程局部存储(TLS)导致无法用于无操作系统的裸机环境,而 0.2 在重构 Context 参数时引发了生态升级动荡与库维护者的严重焦虑。 - [37:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2256s) 在缺乏垃圾回收机制的前提下,手动手写 Future 组合子会导致生命周期约束断裂,借用检查器无法通过,迫使开发者引入不必要的堆分配与运行时检查,使 async/await 成为必须的语言级解法。 - [39:35](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2375s) 最终并入标准库的 std::future::Future 剥离了强制关联的 Error 类型以适应不出错的普通异步逻辑,并正式引入 Pin 机制来解决自引用状态机跨越挂起点的内存移动安全问题。 - [40:55](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2455s) 主流语言通用的前缀 await expr? 在 Rust 中会破坏已有错误传播符 ? 的语法优先级并引发括号地狱,促使社区顶住争议决断采用后缀无括号的 expr.await? 语法。 - [44:13](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2653s) 借助 #[runtime::main] 等属性宏与 wasm-bindgen,Rust 能够消除底层的执行器调度样板代码,甚至在 WebAssembly 中无缝实现 Rust Future 与 JavaScript Promise 的双向嵌套桥接。 - [48:05](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2885s) 不同编程语言的并发机制取决于自身的核心约束:Go 和 Erlang 因拥有 GC 和厚重运行时可选择有栈协程,而 Rust 必须坚守零抽象成本与裸机支持,只能走出属于自己的无栈状态机异步之路。 ## Detailed walkthrough ### [00:03](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=3s) 演讲开场与 Rust 发展史爱好者的自我定位 Steve Klabnik 在 QCon 的舞台上打趣地开场,调侃自己似乎形成了一种固定惯例:每次在 QCon 做关于 Rust 的技术分享,主题永远是“大约三个月后即将正式落地的前沿动态”。本次演讲的主题是《Rust 迈向 async/await 之路》(Rust's Journey to Async/Await),核心就是拆解 Rust 语言如何一步步演化出 `async`/`await`,以及当前正在收尾的关键环节。Steve 介绍了自己的背景——他是 Rust 核心团队(Rust Core Team)成员并负责文档团队,当时就职于 Cloudflare(尽管日常工作并不直接编写 Rust 代码),次日还要主持关于 Rust 以及 Cloudflare Workers 的工作坊。 尽管身处 Rust 核心团队,Steve 特别澄清自己并非这项特性的主导设计者,他将自己形容为一名记录者和“Rust 演进的业余历史学家”,通过长年阅读社区海量讨论、与一线开发者交流,以一种半局外人的视角来呈现这场漫长的技术演进。他特别强调并致敬了 Withoutboats,称其在过去数年里作为核心驱动者,引领了整个特性的设计落地。关于自己的角色,Steve Klabnik 直言:“I like to think of myself as like a amateur historian of rust development I like to keep track of how the language has evolved over time and sort of like deal with these major topics so I was not directly involved in the design I just read all the comments and talked to people over years so I'm kind of giving you my like semi outside perspective on how this went down.” - **演讲者身份**:Rust 核心团队成员、文档团队负责人、Cloudflare 工程师。 - **核心设计功臣**:Withoutboats,过去几年主导推动了 Rust `async`/`await` 的整体机制设计。 --- ### [01:28](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=88s) 并行、并发与异步的基础概念辨析 要讲清楚 Rust 为何最终选择目前的 `async`/`await` 路线,必须先厘清常常被混为一谈的计算概念。在让计算机“同时处理多件事”的技术语境下,术语的混淆极为严重。为此,Steve 先对“任务”(Task)、“并行”(Parallel)、“并发”(Concurrent)和“异步”(Asynchronous)进行了严格界定。首先,“任务”是一个最通用的兜底概念,代表在系统内运行的某段计算过程,它可能是操作系统线程,也可能完全不是线程。 关于并行与并发,Steve 用物理层面的约束做出了鲜明区分:“technically things can only be parallel if you have multiple CPU cores or multiple CPUs.” 物理世界中单个 CPU 核心在同一时刻只能执行一件事,只有具备多个核心或物理 CPU 时,多项计算才能在时间轴上真正同时展开。而并发则是“假装自己拥有多个核心”,正如 Steve Klabnik 所指出的那样:“concurrency means that you like pretend that you have multiple cores but you don't actually have multiple cores so you do a little bit of work on one thing a little bit of work on another thing and another will work on another thing.” 至于异步编程,它并不直接等同于前两者,而是处于一个完全不同的正交维度——它是编程语言层面提供的一种抽象能力,允许开发者编写能够以并行或并发方式执行的代码,打破常规代码自上而下逐行串行执行的固有模式。 ```mermaid flowchart TD accTitle: 并行计算与并发计算的执行模式对比 accDescr: 对比展示多核物理并行与单核分时轮转并发的基本区别 subgraph Parallelism["并行计算 (Parallelism) - 依赖多物理核心"] P_Core1["核心 1: 任务 A 持续执行"] P_Core2["核心 2: 任务 B 持续执行"] end subgraph Concurrency["并发计算 (Concurrency) - 单核交错调度"] C_Slice1["核心 1: 任务 A (切片 1)"] --> C_Slice2["核心 1: 任务 B (切片 1)"] C_Slice2 --> C_Slice3["核心 1: 任务 A (切片 2)"] end ``` - **任务(Task)**:在并行或并发系统中运行的计算单元泛称。 - **并行计算(Parallel Computing)**:严格依赖多核心/多处理器,在物理时间维度上同时独立执行多个任务。 - **并发计算(Concurrent Computing)**:任务在时间线上交替轮转推进,宏观上同时发生,微观上分时复用。 - **异步编程(Asynchronous Programming)**:支持组织非阻塞、可并发/并行调度的语言结构与 API 体系。 --- ### [04:26](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=266s) 任务调度机制:协作式与抢占式多任务 确立了基本定义后,Steve 转向了多任务操作系统的调度模型:协作式多任务(Cooperative Multitasking)与抢占式多任务(Pre-emptive Multitasking)。协作式多任务要求所有任务自觉配合,由当前运行的任务主动决定何时放弃对 CPU 等资源的占用,并让出控制权给其他任务执行。Steve 回忆自己最初接触这套机制,正是在 Windows 95 发布并引入抢占式多任务、或是经典 Mac OS 从 System 7 演进到 Mac OS 8 的年代。 相比之下,抢占式多任务将主动权交给了系统调度器,调度器可以强行中断任何正在运行的计算,把 CPU 移交给其他任务。这构成了计算机体系设计中典型的妥协折中:协作式多任务如果遇到恶意的或写出死循环的程序(Bad Actor),整个系统就会被彻底阻塞,因为恶意任务永远不交出控制权;抢占式虽然对不可信环境非常稳健,但其上下文切换和中断开销也带来了系统复杂度和性能成本。因此,当系统中的所有代码都是可信且合作良好的参与者时,协作式模型反而能换取更轻量、更高性能的执行效率。 - **协作式多任务**:由运行中的任务自主调用 yield 让出执行权;风险在于单个恶意或故障任务会直接拖垮全局。 - **抢占式多任务**:由操作系统内核依据时间片强制剥夺控制权;保障系统整体响应能力,但伴随上下文切换与调度簿记开销。 --- ### [05:45](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=345s) 线程模型抉择:原生线程(1:1)与绿色线程(M:N)的权衡 随后,Steve 深入探讨了对 Rust 早期及现代设计产生决定性影响的关键分歧:原生线程(Native Threads)与绿色线程(Green Threads)。原生线程常被称为 1:1 线程模型,即用户程序所创建的任务与操作系统内核级线程一一对应。而绿色线程则被称为 M:N(或 N:M)模型,指的是由语言运行时自行提供轻量级任务抽象,并将 N 个程序内的绿色任务映射调度到底层的 M 个操作系统原生线程上。 Steve 强调:“Everything's a trade-off there's no free lunch.” 原生线程的优势在于直接利用操作系统内核调度器,内核掌握系统全局所有进程和硬件资源的完整视野,能做出最具全局统筹性的调度决策;此外原生线程历经几十年发展,抽象清晰且被工业界透彻理解。然而其代价是资源开销过大——每个系统线程在 Linux 或 Windows 默认被分配数兆字节(MB)的专用调用栈(Stack Size),导致单台机器能够创建的原生线程总量受到严格的内存限制。 相对地,绿色线程由语言运行时调度,其最大亮点在于运行时深刻理解自家语言的程序结构,可以量身定制调度策略;但荒谬而讽刺的是,“脱离操作系统内核”既是绿色线程的最大长处,也正是它的最大短板。由于运行时的调度行为被遮蔽在用户空间,操作系统的内核调度器无法洞察语言运行时的真实任务分布,二者之间极易在 CPU 时间片分配上产生脱节与冲突,演变成调度器之间的“内战”。这一关于运行时开销与控制权的权衡,直接成为后续 Rust 在网络 I/O 架构与语言核心哲学上做出历史性抉择的根源。 ### [08:59](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=539s) 绿色线程与原生线程的优劣权衡 绿色线程(green threads)的机制可能会变得极其复杂且怪异,但大家普遍谈论的核心优势在于它们非常轻量。通常情况下,绿色线程启动时的栈内存大小仅为原生线程的十分之一到百分之一。这意味着开发者可以创建比原生线程多出数十到数百倍的绿色线程。在围绕绿色线程构建的系统中,一次性启动上百万个线程是非常合理的常规操作;而如果用操作系统原生线程去尝试这种规模,系统内存会瞬间耗尽崩溃。 然而,正因为绿色线程脱离了原生操作系统,也随之带来了一系列严重的缺陷。由于其初始栈极小,一旦绿色线程用尽了这块狭小的栈空间,就必须触发某种扩容机制。各个语言和运行时对这个问题的处理方式各不相同,而这也正是不同绿色线程实现方案分道扬镳的地方:系统需要扩大栈的尺寸、迁移并复制原先的旧栈数据,整个流程伴随着可观的运行时负担。此外,当绿色线程调用 C 语言代码时,C 语言默认要求一个真正、标准的机器调用栈,在绿色线程的小栈与 C 语言原生栈之间来回切换会产生显著的上下文切换开销。 究竟选用哪种线程模型完全取决于目标场景,而这正是 Rust 演进历程的核心叙事线索。正如 Steve Klabnik 概括的那样:“今天我要讲的故事,很大程度上就是关于 Rust 曾经如何坚信其中一种方式是唯一正确的道路,随后又在数年间经历了痛苦的挣扎,最终慢吞吞地全面转向了另一种方式。” ```mermaid flowchart TD accTitle: Green Threads vs Native OS Threads Tradeoffs accDescr: Comparison between lightweight green threads with small stacks and native OS threads with heavy resources. subgraph Green["绿色线程 (M:N)"] G1["超轻量栈空间<br/>(约为原生线程 1/10 ~ 1/100)"] G2["支持百万级高并发<br/>(不会迅速耗尽 RAM)"] G3["缺点: 栈扩容与内存拷贝开销"] G4["缺点: 调用 C 代码时的栈切换成本"] G1 --> G2 G2 -.-> G3 G2 -.-> G4 end subgraph Native["操作系统原生线程 (1:1)"] N1["标准完整栈空间<br/>(与 C ABI 完美兼容)"] N2["由内核原生调度<br/>(无用户态运行时开销)"] N3["缺点: 资源开销沉重<br/>(无法海量并发创建)"] N1 --> N2 N2 -.-> N3 end ``` ### [10:20](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=620s) C10K 问题与 Web 服务器架构的演进 在完成漫长却必不可少的技术术语铺垫后,必须正视为何业界如此关注这些概念,其历史根源正是著名的“C10K 问题”。早在 1999 年,首次系统阐述该问题的网站就提出了划时代的设想:“难道你们不觉得现在的 Web 服务器应当能够同时处理一万个客户端并发了吗?毕竟互联网已经今非昔比,计算机配置也足够强大了——花上 1200 美元就能买到一台 1GHz 主频、2GB 内存并配备千兆网卡的机器。”在 1999 年,当时全网最繁忙的 FTP 站点之一 `cd-rom.com` 已经真正做到了同时承载一万个并发客户端。回顾这个案例带有一种奇妙的时代感:无论是 FTP 还是 CD-ROM,在如今的网络中都已逐渐式微,但在 20 世纪末,如何承载爆炸式增长的用户量成为了互联网基础架构的核心课题。 ```mermaid flowchart LR accTitle: Evolution of Web Server Concurrency Architecture accDescr: Evolution from Apache prefork to multi-threaded worker pools to handle higher concurrency. subgraph Prefork["Apache Prefork 模型 (早期)"] P_Ctrl["Master 监听进程"] -->|"每个连接 fork 一个进程"| P_W1["子进程 1"] P_Ctrl --> P_W2["子进程 2"] end subgraph Worker["Apache Worker 线程池模型 (演化)"] W_Ctrl["Master 控制进程"] --> W_Proc["子进程"] W_Proc -->|"分发到线程池"| W_T1["OS 原生线程 1"] W_Proc --> W_T2["OS 原生线程 2"] end Prefork -->|"减少资源消耗,提升并发"| Worker ``` 为了应对这一挑战,Web 服务器的架构经历了深刻变革。以当时的 Apache 为例,其早期采用的是极为沉重的 pre-fork 模型:主控进程监听连接,每当有请求到来,就派生(spawn/fork)出一个完全独立的子进程来专门处理该连接。这种为每个连接分配完整进程资源的模式开销极大,很快在海量并发前碰壁。随后 Apache 转向了 Worker 模型:一个子进程内部维护一个原生线程池,使用 1:1 的操作系统线程来接手连接。这种线程池方案大幅削减了系统资源的浪费,使得服务器能够在硬件有限的前提下应对更高密度的互联网流量。这一历史演进构成了 Rust 探索异步机制的深层宏观背景。 - 1999 年经典硬件配置基准:单机 1GHz CPU、2GB RAM 及千兆以太网网卡,售价约 1,200 美元。 - 1999 年典型高并发代表站点:`cd-rom.com`,其并发连接数达到了 10,000。 - Apache Pre-fork 模型缺陷:进程创建与上下文切换过于重量级,内存无法承受万级并发。 - Apache Worker 模型优化:子进程内部利用操作系统原生线程池处理并发,提高了资源利用率。 ### [13:15](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=795s) 早期 Rust 的绿色线程与非阻塞网络库 Rust 语言最初由 Mozilla 主导开发,核心诉求是增强 Firefox 浏览器。值得注意的是,Firefox 是一个 HTTP 客户端而非 HTTP 服务端。虽然客户端同样依赖高效的 I/O 来加快网页加载速度,但它不需要像服务端那样同时应付上万甚至上百万的外部并发请求。因此,在项目初期,支撑极限规模的高并发并不是 Rust 的最高优先级。 在此背景下,早期 Rust 标准库包含了一个名为 `green` 的调度模块,采用 M:N 的绿色线程设计。那时的 Rust 深受 Erlang 的影响,并且“在当年它看起来其实远比现在的 Rust 更接近于当今的 Go 语言”。当时的架构设计遵循极简设想:如果开发者需要吞吐性能,只需直接启动无数个绿色线程,每个连接对应一个,运行时自会搞定一切。与此同时,旧版文档中还包含了一个 `net` 模块(文档中甚至因为排版 bug 将同一段描述连续重复打印了两次),该模块被描述为提供“同步但非阻塞(synchronous but non-blocking)”的网络 I/O,支持在标准库中直接进行底层的 TCP 和 UDP 操作。 - 开发初衷:Mozilla 为重塑 Firefox 浏览器客户端而启动 Rust,非服务端超高并发导向。 - 早期并发模型:基于 `green` 模块实现 M:N 调度的绿色线程模型,语法和运行时与 Erlang 和 Go 具有强相似性。 - 标准库网络设计:早期 `net` 库直接对外提供非阻塞但语义上同步的套接字操作。 ### [15:10](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=910s) I/O 象限分类法:阻塞/非阻塞与同步/异步 “同步且非阻塞”这种组合表面上看起来自相矛盾,但从系统设计来看,I/O 的分类并非单轴二选一,而是由“同步 / 异步”与“阻塞 / 非阻塞”构成的四象限结构,其中实际存在三种合理的技术形态: | I/O 模式分类 | 典型语言/环境 | 代码书写风格 | 运行时底层行为 | |:--- |:--- |:--- |:--- | | **同步阻塞 (Synchronous Blocking)** | 早期 Ruby 解释器及绝大多数早期脚本语言 | 线性顺序执行,语义直观 | 遇到 I/O 系统调用即完全挂起线程,等待数据返回,吞吐性能低下 | | **异步阻塞 (Asynchronous Blocking)** | 理论上不存在该组合的实际应用语言 | 编写异步处理回调/轮询机制 | 内部却执行系统阻塞,破坏异步非阻塞意义,属于无意义的伪命题 | | **同步非阻塞 (Synchronous Non-blocking)** | Go 语言、现代 Ruby 运行时、早期 Rust | 纯线性、顺序化书写,与同步代码完全一致 | 运行时在底层透明使用非阻塞系统调用;遇到等待时自动让出调度,用户无感 | | **异步非阻塞 (Asynchronous Non-blocking)** | Node.js、现代前端与现代 Rust | 显式使用回调、Promise 或 `async`/`await` | 底层非阻塞,用户代码必须直面异步时序与事件流,编程心智负担较高 | “同步阻塞”尽管容易编写,但性能极差,注定无法满足现代系统需求;“异步阻塞”则在工程逻辑上不合逻辑。在非阻塞领域,关键差别体现在代码外观与运行时控制权之间的博弈:Node.js 选择了显式的异步非阻塞,代码从结构上就必须处理回调或 Promise;而 Go 和近年的 Ruby 则采取“同步非阻塞”策略——代码表面上看是完全线性的普通程序,底层运行时却透明地将其转为非阻塞调用,这允许老旧项目在不重写业务代码的前提下享受到调度带来的吞吐提升。早期的 Rust 同样选择了同步非阻塞路线,试图隐藏底层异步复杂性。 ### [17:38](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1058s) 绿色线程与系统级系统编程愿景的根本冲突 尽管“同步非阻塞搭配绿色线程”的使用体验非常平滑,但它最终被证明与 Rust 自身的根本目标格格不入。在当年的 Rust 官方网站首页上,该项目被庄重地定义为:“一门运行飞快、杜绝一切崩溃并消除数据竞争的系统级编程语言(systems programming language)。” 这种核心定位直接引发了理念冲突。绿色线程依赖语言附带的重量级用户态运行时(runtime)来进行上下文管理与任务调度,而原生系统线程则由底层操作系统内核原生支持。作为一门追求零运行时开销、对标 C/C++ 的现代系统级编程语言,强行绑定一个厚重的绿色线程运行时,不仅限制了系统与 C-ABI 的直接互操作性,也背离了底层语言对极致受控与硬件贴合度的严苛要求。Rust 团队由此被迫重新审视整个运行时的根本架构。 ### [18:07](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1087s) 双重线程模型的破裂与 RFC 230 的决断 在系统级编程语言中,无法直接调用操作系统提供的线程机制是不可理喻的。Steve Klabnik 指出:“what does it mean to be a systems programming language if you don't offer your users the ability to use system threads like it's like a fundamental API of the OS but we're not gonna let you use it that seemed really like incoherent and so we didn't think that like blocking green threads was really like the answer for rust ultimately”。这直接引爆了 Rust 设计史上的第一次大论战,甚至有开发者以“创建分支(fork)”相威胁。 为了平息争端并兼顾需求,Rust 团队在随后的版本中尝试妥协,同时提供了两个包:基于 1:1 模型的系统原生线程包(`native`)和基于 M:N 模型的绿色线程包(`green`)。官方设计设想了一个统一的运行时抽象接口(runtime interface),允许用户根据具体业务场景自由选择底层实现。然而,实际开发很快证明这种“两者兼得”的理想根本行不通。为了在两种机制之上建立通用的抽象层,系统不得不为绿色线程注入极其繁重的簿记状态(bookkeeping),导致绿色线程彻底丧失了轻量级的优势,沦为“一种体验糟糕且更低效的 1:1 线程副本”。 随着 1.0 正式版的临近,Rust 必须冻结基础 API。继续保留两种线程模型不仅会让编译器与运行时维护变得无比复杂,还会违背系统语言的零成本抽象原则。最终,Rust 团队在 RFC 230 中痛下决心,彻底将绿色线程及内置运行时从语言中彻底剥除。1.0 版本转而采用最精简、最纯粹的系统原生阻塞线程模型。尽管曾有人愤而 fork 了 Rust 代码库,但事实证明只有少数几个人点了星标,根本无人使用。2015 年 5 月,带着这套毫无花巧的 1:1 线程体系,Rust 1.0 正式发布。 ```mermaid flowchart TD accTitle: Rust 1.0 线程模型取舍历程 accDescr: 展示 Rust 如何从早期的纯绿色线程演变为双重运行时抽象,最终因抽象成本过高而在 RFC 230 中全面转向系统原生线程。 subgraph Early["早期探索"] A["仅支持绿色线程 (M:N)"] end subgraph Compromise["双重运行时妥协"] B["抽象 Runtime 接口"] C["native 包 (1:1 原生线程)"] D["green 包 (M:N 绿色线程)"] B --> C B --> D end subgraph Decision["RFC 230 与 1.0 决断"] E["簿记开销沉重<br/>绿色线程丧失轻量优势"] F["Rust 1.0 冻结标准库<br/>彻底移除运行时,仅留系统线程"] end A -->|"无法直接使用 OS 线程引发争议"| Compromise Compromise --> E E --> F ``` - **核心矛盾**:若自诩为系统级编程语言,却向开发者屏蔽操作系统的底层线程 API,在逻辑上是站不住脚的。 - **抽象代价**:为了兼容原生线程而加在绿色线程上的运行时开销,直接抵消了绿色线程的性能收益。 - **历史节点**:RFC 230 正式移除了绿色线程;2015 年 5 月,仅包含原生阻塞线程的 Rust 1.0 正式发布。 --- ### [21:18](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1278s) 1.0 后的性能困境:CPU 密集与 I/O 密集的鸿沟 Rust 1.0 发布后获得了广泛好评,但随之而来的却是更大的应用压力。如果 Rust 只是一个小众实验品,原生阻塞线程或许已经足够;但社区开始大规模尝试用 Rust 构建网络服务。这使得 Rust 再次撞上了类似 1999 年 Apache 式的并发性能瓶颈。开发者用 Rust 搭建高性能网络服务时,底层却不得不为每个连接创建重量级的系统线程,导致高并发场景下的整体性能完全无法匹敌宣传中“与 C 语言一样快”的口号。 问题的本质根源于 CPU 密集型(CPU-bound)与 I/O 密集型(I/O-bound)任务计算特性的根本分野。CPU 密集型任务的速度完全取决于处理器每秒能执行的算术运算量,这类任务最适合使用多核并行(Parallelism)与原生线程;而 I/O 密集型任务的核心在于网络与外设等待,计算速度完全取决于远程服务的响应延迟。如果远端服务器需要 1 秒才能返回,本地即使用最顶级的 CPU 也只能白白闲置等待 1 秒。 对于需要承载海量连接的网络服务而言,最高效的处理方式反而是“什么都不做”。在大多数连接都处于闲置和等待状态时,若为每个连接分配一个独占的系统线程,系统资源(如线程栈内存和上下文切换开销)就会被无意义地耗尽。原生线程模型在处理这种等待型负载时极度低效,使得 Rust 团队意识到必须为 I/O 密集型场景寻找全新的架构解法。 | 计算类型 | 核心瓶颈 | 核心特征 | 线程模型适配度 | |:--- |:--- |:--- |:--- | | **CPU 密集型 (CPU-bound)** | 本地算力(核心主频、并行计算核数) | 处理器全力运转,持续吞吐运算 | 极高(原生 1:1 线程及并行库能充分释放多核性能) | | **I/O 密集型 (I/O-bound)** | 外部响应(网络往返、磁盘及远端响应) | 绝大部分时间处于空闲等待状态 | 极低(为闲置连接保持重量级原生线程会导致资源枯竭) | --- ### [24:25](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1465s) 为何无法借鉴 Go/Erlang,以及基于事件循环的新出路 其他语言早已提出了各自的方案:Go 和 Erlang 采用了在语言运行时内部实现异步 I/O 并对用户代码呈现阻塞接口的做法。在 Go 语言中,大量 Goroutine 会被调度复用到极少数操作系统线程上,遇到 I/O 时由运行时自动切出休眠。这种模式成熟且高效,然而对 Rust 而言却存在一个致命的缺陷:C 语言互操作开销(FFI Overhead)。在绿色线程环境下调用外部 C 语言代码需要切换运行上下文和调用栈,成本极其高昂。 正如 Steve Klabnik 所阐述的差异:“This is totally fine for Go because Go is trying to like put everything in Go and that's also totally fine for Erlang because like it's not that big of a deal you're not using Erlang to crunch numbers but Rust is supposed to be a systems programming language so like if we did green threads what would a systems programming language that's more expensive to call into C and operating system code like mean like that's also incoherent”。作为必须与操作系统及 C ABI 保持零成本集成的系统级语言,Rust 绝不可能容忍跨语言调用的性能惩罚。 排除掉绿色线程之后,业界其实存在另一条技术路径——以 Nginx 为代表的事件驱动(Evented I/O)架构。Nginx 依靠单一主线程配合“事件循环(Event Loop)”机制处理海量并发:底层通过向操作系统注册事件回调,当且仅当数据到达时才唤起对应的处理逻辑,其他处于等待中的连接则完全不消耗活动线程资源。这一无需庞大运行时、仅用单线程即可撑起极高吞吐的并发哲学,为非阻塞网络编程指明了方向;2009 年 Ryan Dahl 在 JSConf 上推出的 Node.js,正是将这种事件循环模式推向通用编程语言生态的标志性产物。 - **语言定位差异**:Go 倾向于重写一切为纯 Go 实现,Erlang 不用于密集数值计算,二者均可容忍 FFI 开销;但 Rust 以零抽象成本和纯粹的系统集成为根本,无法接受跨 C 调用的额外开销。 - **架构范式**:Nginx 与 Node.js 验证了非阻塞 I/O 配合事件循环的扩展能力,通过单个线程驱动就绪事件,彻底避免了重量级系统线程因等待 I/O 而产生的资源浪费。 ### [26:57](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1617s) Node.js 的崛起与“回调地狱”的困境 Node.js 之所以诞生并引起巨大轰动,核心就在于事件驱动与非阻塞 I/O(Evented Non-blocking I/O)。在早期的演讲幻灯片中,Ryan Dahl 明确将这一机制列为 Node.js 区别于传统后端技术的核心支柱。在传统的阻塞式 I/O 中,调用类似 `fs.readFileSync` 的 API 会直接挂起当前线程,直到数据完全读取完毕;但在高并发场景下,为了让单线程事件循环(Event Loop)能够高效调度成千上万的网络请求,开发者必须将阻塞调用改写为非阻塞回调。 这种非阻塞回调模式在代码规模较小时看似优雅简单,只需传入一个闭包处理返回结果。然而,一旦业务逻辑变得复杂,代码便会不断向右层层缩进,形成臭名昭著的“回调地狱”(Callback Hell)。社区甚至专门设立了 `callbackhell.com` 网站来展示这种可怕的代码结构,演讲者还打趣地提到社区中有人把《街头霸王》里的隆(Ryu)发射波动拳(Hadouken)的动作做成梗图,形象地嘲讽代码被推向屏幕最右侧的视觉灾难。 ```mermaid flowchart LR accTitle: Callback Hell vs Promise Chaining accDescr: 对比传统嵌套回调代码的向右漂移与使用 Promise 扁平化链式调用的差异。 subgraph CB["回调地狱(向右漂移)"] direction TB C1["fs.readFile"] -->|"嵌套闭包"| C2["处理回调 1"] C2 -->|"再嵌套闭包"| C3["处理回调 2"] C3 -->|"继续向右缩进..."| C4["最终逻辑"] end subgraph PR["Promise 链式(垂直排列)"] direction TB P1["createPromise()"] -->|".then(step1)"| P2["执行步骤 1"] P2 -->|".then(step2)"| P3["执行步骤 2"] P3 -->|".then(step3)"| P4["执行步骤 3"] end ``` 为了解决不断向右漂移的代码痛点,JavaScript 社区引入了 Promise。Promise 本质上是一个代表未知未来结果的对象,具备三种状态:未完成的 Pending、成功解析的 Fulfilled 以及失败的 Rejected。通过 `.then()` 链式调用,原本层层深嵌的闭包被展平为垂直排列的代码流。虽然这在语法上减轻了缩进压力,但随之而来的却是长串的 `.then().then().then()` 链条,依然存在许多不够直观的人体工学缺陷。 - Node.js 的核心定义是基于 V8 引擎的跨平台服务器端 JavaScript 环境,其根本设计哲学完全建立在事件驱动与非阻塞 I/O 基础之上。 - 阻塞式调用如 `fs.readFileSync` 会挂起线程,而非阻塞调用 `fs.readFile` 依赖传入闭包并在事件就绪后交由事件循环回调。 - “回调地狱”导致严重的右向漂移(Rightward drift),而 Promise 仅仅是将横向缩进转换成了纵向排列的闭包链接。 --- ### [29:41](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1781s) 从 Twitter 的 Scala 实践到 Rust 0.1 零成本 Future 面对高并发 I/O 带来的扩展性瓶颈,Twitter 也曾遭遇过惨痛教训。他们早期的系统构建在 Ruby on Rails 之上,在流量暴增后不堪重负,最终决定将核心网络基础设施全面重写至 JVM,并重度押注 Scala 语言。Twitter 随后发表了一篇影响深远的论文——《Your Server as a Function》,详细阐述了他们如何利用 Future 概念构建极高性能的网络服务。Rust 团队敏锐地捕捉到了这一动态,认为静态类型的 Scala 能够成功落地的方案,同样能够为强类型的 Rust 提供极佳的借鉴。 2016 年 8 月,Rust 社区正式发布了“零成本 Futures”(Zero-cost Futures)。在 2016 年发布的 `futures 0.1` 版本中,核心抽象是一个名为 `Future` 的 Trait。该 Trait 定义了两个关联类型:`Item` 和 `Error`,并提供了一个核心的 `poll` 方法以及一系列诸如 `and_then` 的组合算子。这个设计让开发者能够像使用 JavaScript 的 Promise 一样,通过链式调用将异步操作串联起来,构建出一个庞大的状态机图,最终将其作为任务(Task)提交给执行器(Executor)调度执行。 ```mermaid flowchart TD accTitle: Rust Future 0.1 执行模型 accDescr: 展示 Rust futures 0.1 如何通过组合算子链式构造并提交给 Tokio 执行器执行。 subgraph Build["编译期构建状态机"] A["TcpListener::incoming()"] -->|"and_then"| B["处理数据流 Future"] B -->|"map_err"| C["错误捕获 Future"] end subgraph Exec["运行时调度执行"] C -->|"封装为单个 Task"| D["tokio::run(server)"] D -->|"循环轮询"| E["Future::poll()"] end ``` 在演讲中,Steve Klabnik 回顾了那个时代 Rust 异步编程的大胆尝试,并指出了命名上的幽默分歧:“there's also a ton of other convenience functions on here like and then as you get this code at the bottom that looks very similar to that promise code except for says and then instead of then cuz you know can't name everything the same way across languages that would be too hard”。 - Twitter 弃用 Ruby on Rails 并全面拥抱 Scala 与 JVM,催生了经典论文《Your Server as a Function》。 - Rust 团队在 2016 年 8 月正式宣布引入 Futures,力求在不牺牲系统级语言控制力的情况下达成现代异步模式。 - `futures 0.1` 中的 `Future` Trait 核心 API 依赖 `fn poll(&mut self) -> Poll<Self::Item, Self::Error>`,并提供 `and_then`、`map` 等链式组合算子。 --- ### [31:38](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=1898s) 两种哲学:JavaScript Promise 与 Rust 惰性 Future 的内存机制 虽然 JavaScript 的 Promise 与 Rust 的 Future 概念相近,但由于两门语言的底层设计哲学截然不同,其实际运行机制存在天壤之别。JavaScript 内置了重量级运行时,所有代码默认假定异步,因此 Promise 一旦被创建就会立即在后台开始执行(Eager Execution)。这种机制虽然表面上直观,但在系统级视角下代价高昂:由于创建链式调用时底层的任务已经在并发推进,系统无法在初始阶段获知整条调用链的完整规模,必须为每一个串联的异步步骤单独进行一次堆内存分配。 相反,Rust 是一门没有内建运行时、追求极致低开销的系统级语言,必须提供比肩 C 语言的底层控制能力。因此,Rust 的 Future 是完全惰性(Lazy)的:构建一整套 Future 链条时,它完全不进行任何后台计算,直到开发者显式地将其投递给执行器(如 Tokio)并由执行器主动调用 `poll` 方法时,计算才会启动。更具革命性的是,Rust 编译器能够在编译阶段静态分析整条任务链,精确推导整个异步状态机所需占用的内存大小。这让 Rust 仅需单次内存分配(甚至在限制条件下可以做到完全零动态内存分配的静态内联),彻底避免了协程/绿色线程栈内存动态收缩与再分配的巨大开销。 在分析 Rust 与 JavaScript 这两种截然相反的执行模型与性能差距时,Steve Klabnik 评价道: > “And that also means that unlike promises in JavaScript you build up this chain of futures and nothing actually happens with them at all until you explicitly hand them over to an executor object and say please run this for me in the background and so they don't do anything until the pull method is called and so this is a little more complicated but it's also extremely efficient because you know how big the task is going to be when you create it.” | 比较维度 | JavaScript Promise | Rust Future (0.1 及后续) | |:--- |:--- |:--- | | **执行模型** | 饥渴型(Eager):创建即在后台启动运行 | 惰性(Lazy):直到执行器调用 `poll` 前不做任何工作 | | **运行时依赖** | 强绑定 JS 引擎与全局事件循环 | 纯库实现(Library-only),无内置运行时要求 | | **内存分配** | 链式调用的每一步都需要独立的动态堆分配 | 编译期确定整体状态机尺寸,单次分配甚至纯静态分配 | | **硬件适应力** | 局限于拥有垃圾回收与运行时的高层环境 | 零成本抽象,可裁剪移植至嵌入式与裸机(Bare-metal)环境 | --- ### [34:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2076s) 0.2 版本的折戟与生态分裂的阵痛 尽管 `futures 0.1` 配合 Tokio 开启了 Rust 高性能网络编程的大门,但早期的架构设计很快暴露出严重的缺陷。其中最致命的弱点在于,`futures 0.1` 极度依赖线程局部存储(Thread-Local Storage, TLS)来维护任务唤醒与通知等底层簿记信息。这直接宣判了它无法在没有操作系统支持的嵌入式设备、操作系统内核或裸机(Bare-metal)环境中工作,违背了 Rust 追求全场景系统编程的核心准则。 为了解决这个问题,设计团队推出了 `futures 0.2`,试图在 `poll` 方法中显式引入一个上下文参数(`task::Context`),从而消除对 TLS 的硬性依赖。然而,这一重构引发了开源社区关于向后兼容与版本演进的激烈争论。由于 0.2 只是一个过渡性的试验切片,核心团队已经在筹划更加激进的 0.3 版本,这导致生态维护者陷入两难:究竟该不该花费巨大力气升级到 0.2? 针对这次生态更新所带来的焦虑,演讲者回顾了当时的困境: > “And then people are like that seems a little bad cuz like you need to try it out in order to use it but your libraries update how are you supposed to try it out and then if it's gonna change again why would anyone try it out in the first place this person was smart but” - `futures 0.1` 依赖 TLS 进行任务通知管理,导致裸机与无操作系统(No-std)环境无法兼容。 - `futures 0.2` 引入了显式的 `Context` 参数试图解耦环境依赖,但破坏了已有的 API 接口稳定性。 - 当时的核心开发成员 Aaron Turon 曾公开发文建议生态库不必全盘迁移到 0.2,因为 0.3 版本在数月内就会发布,这一表态加剧了生态维护者对于重复迁移成本与 API 动荡的普遍担忧。 ### [03:05:36](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=11136s) 异步语法的演进与 Rust 无 GC 带来的独特困境 讲座回顾了异步编程在主流语言中的演进历程。以 JavaScript 为例,代码最初依赖回调函数(callbacks),随后演化为链式调用的 Promise,以避免层层嵌套的“向右倾斜”;而最终由 C# 首创、随后被 JavaScript 采纳的 `async/await`,则允许开发者用看似同步、自上而下的阻塞式写法来编写实际上的非阻塞异步代码。在 C# 和 JavaScript 这类拥有垃圾回收器(GC)的语言中,`async/await` 很大程度上只是一项提升开发体验的“便利性语法糖”,因为底层的手写 Promise 虽显笨重却能稳定运行。然而,当 Rust 团队在 Futures 0.2 上深入探索时,才恍然意识到:`async/await` 对 Rust 的意义远比对其他语言深远得多。 由于 Rust 没有垃圾回收机制,手写 Future 链条会引发严峻的所有权与生命周期灾难。在传统的同步读取接口中,函数接收 `&mut self` 和缓冲区切片并返回读取字节数,逻辑简单直观;但若将其完全转换为基于 Future 组合子的异步签名,代码复杂度会瞬间失控,其样板代码甚至长到一张幻灯片都放不下。根本症结在于:Future 的“创建阶段”与交由执行器运行的“执行阶段”完全分离,生命周期约束的脱节使 Rust 编译器根本无法推导内存是如何被使用的,只能施加最严苛的借用限制。 为了让编译器通过,开发者不得不退而求其次,在手写代码中引入大量不必要的堆内存分配(Heap Allocation)与运行时检查。这就重演了早期“绿色线程 vs 原生线程”的悲剧:理论上极致轻量高效的零成本抽象,在实际编写现实代码时却变得极度低效。正如 Steve Klabnik 所总结的那样:“So we're now back to a place where like this is not fast and it's hard to write so life is terrible, but async/await to the rescue.” ```mermaid flowchart TD accTitle: Rust 手写 Future 的困境与 async/await 的解决 accDescr: 对比手写 Future 链条与 async/await 编译期状态机的区别 subgraph ManualFutures ["手写 Future 组合子 (Futures 0.1/0.2)"] A["创建 Future 阶段"] -->|"生命周期约束断裂"| B["交由 Executor 执行"] B --> C["借用检查器无法推断内存安全"] C --> D["被迫引入大量堆分配与运行时检查"] D --> E["性能下降且代码极其痛苦"] end subgraph AsyncAwait ["async/await 原生方案"] F["编写近乎同步的代码结构"] --> G["编译器自动生成状态机与生命周期"] G --> H["Pin 与 Borrow Checker 协同"] H --> I["零成本抽象与内存安全兼得"] end ``` ### [03:09:35](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=11375s) 标准库 Future 的重构:解耦错误处理与 Pin 的引入 有了 `async/await` 后,编译器与借用检查器终于能够理解跨越挂起点的数据交互,开发者只需在逻辑外层套上一个异步块(async block)并加上挂起标记,代码便能恢复与同步版本几乎一致的可读性。然而,设计层面依然横亘着历史包袱。在早期的 Futures 设计中,团队先入为主地认为 Future 主要服务于 I/O 操作,因此将错误类型(Error)硬编码进了 Future 的 trait 定义里。但事实证明,异步模式一旦引入就会“感染”整个代码库,许多操作本质上是绝对不可能失败的(infallible);强制绑定的错误类型导致大量根本不会出错的逻辑充斥着无意义的样板代码。 最终并入 Rust 标准库的 `std::future::Future` 作出了重大重构:剥离了强制的错误类型,仅保留单一的 `Output` 关联类型。如果某个异步任务可能失败,只需将标准库现成的 `Result<T, E>` 作为其 `Output` 进行组合即可。同时,函数签名中正式引入了 `Pin`(固定)机制和执行上下文 `Context`。关于这套晦涩的机制,Steve Klabnik 直言自己完全可以就 Pin 单独做一场长达 45 分钟的演讲,但对绝大多数应用开发者而言它在幕后默默生效即可:“Pin is part of how async/await teaches the borrow checker how the code works; I'm not going to get into it because frankly I can give you a 45-minute talk just on Pin itself.” - **旧版设计缺陷**:将 Future 与 I/O 绑定,强行要求实现 `Item` 和 `Error` 两种关联类型,导致无错误场景出现冗余模板代码。 - **标准库设计方案**:`std::future::Future` 仅定义 `type Output`,需要报错时利用 `Output = Result<T, E>` 自由组合。 - **Pin 的核心职责**:解决自引用结构体在内存中的移动安全问题,让借用检查器能够验证跨越挂起点的局部引用,普通业务开发者无需直接操作。 ### [04:00:55](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=14455s) 社区大论战:语法分歧与最终敲定的 `.await` 当核心语义理顺后,Rust 社区却遭遇了一场声势浩大的“语法大自行车棚建设”(bikeshedding)。几乎所有主流语言(如 C#、JavaScript)都采用前缀语法 `await expr`。然而在 Rust 中,异步 I/O 极度依赖用于错误传播的后缀操作符 `?`。如果照搬前缀形式,当开发者写下 `await expr?` 时,按常规优先级解析会变成 `await (expr?)`,但实际绝大多数场景需要的却是 `(await expr)?`(先等待完成,再解包校验错误)。如果要打破常规操作符优先级,不仅语言设计规则难以自洽,而且在方法链式调用时会出现严重的反向嵌套与括号灾难。 这场关于语法的讨论迅速演变成 Rust 社区有史以来最激烈的争吵之一。在官方 Discourse 论坛上,单个讨论帖动辄涌入数百条回复;Reddit 讨论串层出不穷;开发者的收件箱被数以千计的邮件淹没。有人甚至半开玩笑地提议引入倒问号(¿)来专门处理不同优先级的错误展开。每个人都有自己偏爱的方案,同时又对自己不喜欢的方案深恶痛绝,没有任何一个语法提案能获得绝对压倒性的共识。 为了不让这项关乎语言命运的关键特性无限期搁置,核心团队最终做出了决断:采用后缀关键字 `.await`(如 `expr.await?`)。尽管这个类似字段访问却无需括号的后缀关键字在发布之初遭到了部分开发者的强烈抨击,但随着实际投入使用,其在链式调用与错误处理中的人体工学优势逐渐显现。Steve Klabnik 坦承这虽然不是他最初个人的首选,但在语言工程中必须打破僵局:“Some people really viscerally hate this; other people were like, it seems weird at first and then 15 minutes later I forgot about it and it's fine. I think that in the end that's really how it's gonna happen, but we ended up with a weird await syntax, it just happens it's the best choice for Rust.” - **前缀 `await` 的语法死结**:`await foo()?` 极易引起结合律歧义,若强制改变优先级则会破坏语言语法的整体一致性;在链式调用如 `(await (await foo()).bar()).baz()` 下括号可读性极差。 - **论战规模**:Discourse 论坛单帖产生近 500 条密集辩论,邮件列表和 Reddit 线程几乎被 `async/await` 的语法提议全面攻占。 - **`.await` 的设计考量**:采用无括号的后缀关键字形式,与已有的后缀操作符 `?` 和方法调用自然融合,实现从左向右的流畅流水线阅读体验。 ### [44:13](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2653s) 语法糖人体工学与 WebAssembly 中的跨语言互操作 除了核心语言特性的演进,Rust 生态系统围绕 async/await 进行了大量提升开发人体工学(ergonomics)的工作。在以往的底层模型中,开发者必须显式将 Future 提交给 Executor(执行器)调度运行。而诸如 `runtime` 这类 crate 引入了属性宏,允许直接在 `main` 函数上添加类似 `#[runtime::main]` 的注解。表面上看,编写的代码几乎与 Node.js 类似,完全不需要手动管理底层执行器,但宏在编译期会将代码展开并重写为驱动 Future 运行的实际逻辑。 ```mermaid flowchart LR accTitle: JS Promise 与 Rust Future 在 WebAssembly 中的转换链 accDescr: 展示 JavaScript Promise 转换为 Rust Future 并在处理后转回 Promise 的全流程 subgraph JS_Env["JavaScript / WebAssembly 边界"] direction TB P1["JavaScript Promise<br/>(数据库 API 返回)"] P2["JavaScript Promise<br/>(返回给前端调用)"] end subgraph Rust_Wasm["Rust (Wasm 运行时)"] direction TB F1["JsFuture::from(promise)<br/>转换为 Rust Future"] AW[".await 异步暂停并处理数据"] F2["future_to_promise(future)<br/>转换回 JS Promise"] end P1 --> F1 F1 --> AW AW --> F2 F2 --> P2 ``` 更令人惊叹的应用场景出现在 WebAssembly(Wasm)领域。Steve Klabnik 展示了他在实际项目中编写的代码片段:在浏览器端的 Rust 代码中直接调用数据库的 JavaScript API,该调用返回一个 JavaScript Promise。Rust 借助 `JsFuture::from` 将其包装成 Rust 的原生 Future,接着直接在其上调用 `.await` 进行暂停与恢复;在数据处理完毕后,再通过 `future_to_promise` 将 Rust Future 转回 JavaScript Promise 并返回给调用方。Steve 调侃道: > “是的,这段 Wasm 代码里是一个被包装进 Rust Future、再被包装进 JavaScript Promise、接着穿过 Rust 返回进 WebAssembly、最终回到 JavaScript 的 JavaScript Promise,而且它完全正常工作——我猜这取决于你对‘正常’的定义。” - `#[runtime::main]` 等属性宏消除了开发者手动初始化 Executor 并阻塞 Future 的样板代码。 - 通过 `wasm-bindgen` 生态中的转换函数,Rust 能够无缝桥接浏览器 Event Loop 与 Rust 自身的异步任务系统。 - 演示代码并非纯理论构想,而是已经实际部署在 Steve 的个人网站生产环境中运行。 --- ### [46:24](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2784s) 历时九年的落槌定音:从稳定发布到性能碾压 在梳理了长达近十年、遍历几乎所有已知并发方案的曲折历程后,async/await 终于迎来了稳定的曙光。虽然演讲幻灯片上原本写着“预计在 Rust 1.37 或 1.38 发布”,但 Steve 兴奋地向现场分享了当天早上的最新进展:Rust 核心团队成员 Without Boats(boats)刚刚发推确认,困扰团队的最后一个微小问题已彻底解决,async/await 确定将在 2019 年 8 月中旬发布的 Rust 1.38 中正式稳定落地。 > “在某种意义上这可以说是从 90 年代末开始的,不过那还在 Rust 诞生之前;从技术上讲大约始于 2010 到 2011 年左右,八九年之后,我们终于摸清了如何在 Rust 中高效地实现 I/O,而且它确实有效。” 这一方案的性能收益在行业基准测试中得到了验证。在最新的 TechEmpower Web 框架性能基准测试中,基于 Rust 的异步网络框架 Actix 展现出了统治级优势。其吞吐表现不仅位列榜首,而且紧随其后的第二名框架性能仅达到其 62% 左右。历经多年的摸索与试错,Rust 最终在保持极高抽象人体工学的同时,压榨出了底层硬件极致的网络服务吞吐量。 - 稳定版落地窗口敲定在 2019 年 8 月 15 日左右随 Rust 1.38 发布。 - TechEmpower 测试中,Actix 框架在多项指标中拉开第二名将近 40% 的断层式差距。 --- ### [48:05](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2885s) 终局复盘:系统级异步设计的两点核心启示 在演讲的最后半分钟,Steve Klabnik 总结了 Rust 这一漫长探索过程给软件工程界带来的两条核心经验。其一,构建世界顶级的底层 I/O 运行机制绝非朝夕之功,需要长达数年的实验、踩坑、推翻与重构。其二,不同编程语言所承载的设计约束(Constraints)截然不同。 > “这就意味着你不能简单地争论说 Go 或 Erlang 的风格就比 Rust 更好或者更差,它们在各个方向上都有各自的优缺点。” 如果开发者仅仅因为在某一门语言中习惯了某种范式,就指责另一门语言采用不同路线是“错误的”,这种看法是片面的。Go 拥有运行时和垃圾回收,因此可以选择有栈协程(M:N 绿色线程);而 Rust 必须坚守“零成本抽象”、无垃圾回收、支持裸机与嵌入式环境的根本底线,这就注定它无法直接抄袭其他语言的现成作业,必须走通一条属于自己的无栈 Future 与状态机驱动之路。 ## Notable quotes > [48:05](https://www.youtube.com/watch?v=lJ3NC-R3gSI&t=2885s) 你不能简单地争论说 Go 或 Erlang 的风格就比 Rust 更好或者更差,它们在各个方向上都有各自的优缺点。 — Steve Klabnik ## Mentioned Steve Klabnik, Withoutboats, Aaron Turon, Ryan Dahl, Rust, Cloudflare, Mozilla, Firefox, Twitter, Node.js, Go, Erlang, Scala, C#, JavaScript, Ruby, Ruby on Rails, Apache, Nginx, Tokio, Actix, WebAssembly, TechEmpower, QCon ## Video description Video with transcript included: http://bit.ly/2XcjHbj Steve Klabnik gives an overview of Rust’s history, diving into the technical details of how the design has changed, and talks about the difficulties of adding a major new feature to a programming language. This presentation was recorded at QCon New York 2019: http://bit.ly/2KFk7SO The next QCon is QCon London 2020 – March 2-4, 2020: http://bit.ly/2VfRldq For more awesome presentations on innovator and early adopter topics check InfoQ’s selection of talks from conferences worldwide https://bit.ly/2tm9loz #Rust #Async #ProgrammingLanguage [Source](https://www.youtube.com/watch?v=lJ3NC-R3gSI)

Source quotes

[48:05] Steve Klabnik: 你不能简单地争论说 Go 或 Erlang 的风格就比 Rust 更好或者更差,它们在各个方向上都有各自的优缺点。

Source quotes

[35:36] Steve Klabnik: async/await is actually more significant to Rust than in other languages.

Original transcript

[0:03] hi everybody I'm Steve and this is rust [0:06] journey to async/await I sort of been [0:09] this theme have given a lot of talks [0:10] about rustic Yukon and it seems like I'm [0:13] always talking about something that's [0:14] happening like three months in the [0:15] future so this sort of continues on that [0:17] trend of things you know as mentioned [0:19] this is about how we arrived at a single [0:22] weight and like the final couple little [0:25] steps that are happening as well I'm [0:28] giving this talk because I'm on the rust [0:29] team in general I'm on the rust core [0:31] team and in charge of the documentation [0:32] team I work at CloudFlare although not [0:35] on rust stuff doing two workshops [0:37] tomorrow one on rust and helping out [0:40] with one on cloud floor workers so I [0:41] don't know if I'll see any there while I [0:44] am on the rust team I want to emphasize [0:46] that I was not like directly involved in [0:48] all of this work I am primarily doing [0:50] this as a like reporting capacity I like [0:53] think of myself as like a amateur [0:55] historian of rust development I like to [0:57] keep track of how the language has [0:58] evolved over time and sort of like deal [1:00] with these major topics so I was not [1:03] directly involved in the design I just [1:05] read all the comments and talked to [1:06] people over years so I'm kind of giving [1:08] you my like semi outside perspective on [1:10] how this went down most specifically a [1:12] lot of people were involved in making a [1:14] single weight and rust happened but a [1:15] special shout-out is deserved from [1:17] without boats who is the person who has [1:19] been primarily driving the design over [1:20] the last couple years so you know votes [1:23] is awesome and wonderful okay to start [1:28] off to figure out how we actually get to [1:30] a single weight in rust we sort of have [1:33] to talk about like what asynchronous [1:34] programming actually is and so it turns [1:37] out that this is really hard to talk [1:39] about because there are so many [1:40] different options in the space of trying [1:43] to make the computer do more than one [1:44] thing at once and so the start of this [1:46] talk is gonna involve a lot of [1:47] definitions just to make sure we're all [1:49] on the same page so so we're gonna start [1:52] off with there so people talk about [1:55] asynchronous computing a sort of along [1:57] with these other two things which is [1:58] parallel computing and concurrent [2:00] computing so people get these three [2:02] definitions mixed up all the time but [2:03] just to be clear parallel computing is [2:06] when you're able to do multiple things [2:07] at once concurrent programming is we're [2:09] Nabal to do multiple things but not at [2:11] once and asynchronous programming is [2:13] actually unrelated to those two things [2:16] it's a totally different axis [2:17] so let's give into this a little bit [2:18] more briefly there's also this other [2:21] term called a task and so this is sort [2:24] of a generic term for some sort of [2:26] computation running inside of a parallel [2:28] or concurrent system so this may mean a [2:30] thread this may not mean a thread this [2:32] can mean sort of anything it's kind of [2:34] just a generic catch-all term for I'm [2:36] doing some sort of thing inside of an [2:39] asynchronous or parallel or concurrent [2:41] system so here means a task a bunch it's [2:43] very generic sort of word ok so parallel [2:46] computing technically things can only be [2:48] parallel if you have multiple CPU cores [2:50] or multiple CPUs it shows a lightning [2:53] bolt for a computational task just [2:55] because I don't know it seemed cool but [2:57] we sort of have this like timeline of [2:58] execution and so you have these two [3:00] things and they're truly executing [3:02] independently this is why this is only [3:04] possible with multiple cores or multiple [3:06] CPUs because you fundamentally can't [3:08] make one processor do two things at once [3:10] because of physics like it's physically [3:13] doing a thing but if you have multiple [3:15] of them then they can be doing two [3:16] independent things in one system so you [3:18] get parallelism this way the second one [3:21] is concurrent programming and [3:22] concurrency means that you like pretend [3:25] that you have multiple cores but you [3:27] don't actually have multiple cores so [3:28] you do a little bit of work on one thing [3:29] a little bit of work on another thing [3:31] and another will work on another thing [3:33] and so these two things like all of [3:36] these options can also be combined so [3:38] for example in a lot of systems you have [3:40] different parallel tasks that are [3:43] executing concurrent things inside of [3:46] the two of them so there's like all [3:47] sorts of ways you can fit these sort of [3:48] things together [3:49] and so when we talk about asynchronous [3:52] programming we're talking about language [3:55] features that sort of let you write code [3:57] that is able to be executed in a [4:00] parallel or concurrent way and that's [4:04] sort of because you need the ability to [4:05] say like doing things simultaneously if [4:07] you think about the way that code [4:08] normally executes you have like the [4:10] first thing and the second thing the [4:12] third thing and they follow down in [4:13] lines in most forms of programming not [4:15] all and so like you need some sort of [4:18] way to be like actually execute these [4:19] things at the same time and so that's [4:22] asynchronous api's okay I'm almost done [4:26] with terms I swear and then we can [4:28] actually productively talk about this [4:31] cooperative and pre-emptive multitasking [4:35] so cooperative multitasking is when all [4:38] of your tasks have to cooperate to [4:40] operate synchronously asynchronously so [4:42] each task sort of decides when it is [4:45] willing to give up the particular [4:46] resource and have another task execute [4:49] instead this is the way that like I [4:51] originally learned about this because [4:53] like I think it was like Windows 95 came [4:56] out and had pre-emptive multitasking or [4:58] like Mac OS 8 instead of 7 or whatever [5:00] and pre-emptive multitasking the system [5:03] decides when you yield to other tasks so [5:05] this means that like any computation can [5:06] be interrupted in the middle and handed [5:08] off to something else this is like a [5:11] very interesting trade-off because in a [5:13] system with cooperative multitasking if [5:15] you have a bad actor they can really [5:17] mess everything up because what happens [5:18] if they never decide to give up control [5:20] of the situation then they just hog all [5:22] the resources so in like an untrusted [5:24] system you often want pre-emptive [5:25] multitasking but being able to do that [5:27] also adds some overhead and some [5:29] complexity and some other things and so [5:31] if you were have a system where you know [5:32] that you have only good actors then [5:34] cooperative multitasking can be a little [5:36] bit better it just really depends but [5:38] these two things are sort of like at [5:40] odds and then finally I think at my big [5:45] terminology block native versus green [5:47] threading this one gets like even more [5:49] complicated I'm not going into all the [5:51] differences here you also hear things [5:53] like co-routines and stack lists source [5:55] tactful co-routines and all these other [5:57] things but at its core there's this sort [6:00] of like dichotomy between native threads [6:01] which are sometimes called one-to-one [6:03] threading and these are tasks that your [6:06] operating system provides as an API so [6:08] if you're running sort of a process [6:10] you're able to say within that process I [6:12] would like to execute some tasks and so [6:15] it's called one-to-one threading because [6:17] you get one task for one operating [6:20] system tasks which sort of implies green [6:22] threads which comes next also called end [6:25] to M or M to end threading I don't see [6:27] it consistently used sometimes people [6:29] put the end first and sometimes M first [6:31] I don't know but these are when your [6:32] programming language or its runtime [6:34] provides you with a task abstraction and [6:36] it Maps a certain number of its own [6:38] tasks on to the operating systems [6:41] threads so you get n [6:43] program threads to em system threads so [6:45] that's why it's n2m and why the other [6:47] one is one to one because there's a [6:48] correspondence a lot of different [6:51] programming languages are using green [6:52] threads and have for a long time both of [6:55] these things are interesting and this [6:57] particular set of things really gets to [6:59] the core of why rust made the decisions [7:01] that it made in order to do async [7:04] specifically there on Io so there's some [7:06] advantages and disadvantages to both of [7:09] them everything's a trade-off there's no [7:10] free lunch native threads have the [7:13] advantage that they're part of your [7:15] system and so the operating system is [7:16] able to handle it scheduling and that [7:19] means that it can see every task that's [7:21] securing on the system and make like the [7:23] appropriate choices to do the scheduling [7:25] because it has total visibility into [7:26] everything that's running they're also [7:28] very straightforward and well understood [7:30] like threads are an old technology we've [7:33] been used for a long time they are hard [7:36] to use sometimes but in terms of like an [7:37] API they're not that complicated [7:39] and they're like super well understood [7:41] for because they've existed for a long [7:42] time however they also have some [7:44] disadvantages and these are also system [7:46] independent so if you look at like [7:48] Windows versus Linux for example you get [7:50] some small different differences here [7:51] but I'm not gonna go super deep into [7:53] those details but the defaults around [7:55] native threads can be kind of heavy and [7:58] there's a relatively limited number that [7:59] you can create due to those sizes so [8:02] your operating system needs to keep [8:03] track of all these different threads and [8:05] so it has its own structures that use [8:06] this for bookkeeping and they also have [8:08] a the primary resource that people are [8:09] concerned with the threads is the stack [8:11] size so sort of like the amount of [8:13] memory each thread can take and by [8:15] default that's relatively large on Linux [8:18] and Windows and so keeping track of that [8:20] overhead you know every time you create [8:22] a thread has them overhead so you can [8:24] only create so many of them and given [8:27] they only have so much memory and you [8:28] only have so much memory you can only [8:29] create so many of them green threads on [8:31] the other hands their advantages is [8:34] they're not part of the overall system [8:35] which is like Ghana hilarious given that [8:37] that's also being part of the system is [8:39] an advantage it's also a disadvantage [8:41] since your runtime handles scheduling it [8:44] can know and sort of prioritize the [8:45] details about your program and how the [8:47] bits of your program operate but sort of [8:50] like the weird downside of that is now [8:52] you've obscured this aspect from the [8:53] overall system so you might get into a [8:55] battle but [8:55] your operating systems the scheduler and [8:57] then your program scheduler and so that [8:59] can get weird and complicated but the [9:02] major advantage that most people talk [9:04] about is that they're significantly [9:05] lighter weight so usually green threads [9:07] start off with a stack size that's like [9:09] a tenth to a hundredth of the size of a [9:11] native thread which means you can create [9:12] tens to hundreds more of them is not [9:15] uncommon in systems that are built [9:16] around green threads you start up say [9:18] like a million threads like that's [9:20] totally reasonable but it's not [9:21] something you would want to do with an [9:22] operating system thread because you [9:24] would run a ram and so there's some [9:27] disadvantages there largely around the [9:29] fact that these are not part of the [9:30] native system anymore [9:32] and so if your green thread runs out of [9:35] that very small amount of stack then has [9:37] to do something there are many different [9:39] ways to handle this and this is a [9:40] largely where all the different [9:41] implementations of green threads kind of [9:43] break down but it needs to grow and [9:45] change the size of that stack and then [9:47] copy the old stack over and do all this [9:48] stuff and so that can cause some issues [9:50] and then also when you call into C code [9:52] C expects like a real actual stack and [9:55] so this can introduce some overhead when [9:58] you have to switch between the stack [9:59] from your green thread and the stack [10:01] from the native thing and so that can be [10:04] like a problem so which one of these is [10:07] appropriate it depends entirely on what [10:09] you're trying to do and largely the [10:11] story that I'm gonna be telling you [10:12] today is about how rust thought that one [10:14] of these things was the right way to do [10:15] things and then painfully over years [10:17] slowly switch to the other one sort of [10:20] um okay so why why do we care about any [10:24] of this stuff at all this is the end of [10:26] my giant definitions block sorry there's [10:28] just so many things here that you have [10:30] to like piece together to really like [10:31] form a coherent story about this I [10:33] needed to like start off by doing that [10:36] there's this old thing called the C 10k [10:38] problem and I took the screenshot this [10:41] was the website that first introduced me [10:42] to this problem it's a little old if you [10:45] can't tell it's time for web servers to [10:48] handle ten thousand ten thousand clients [10:51] simultaneously don't you think after all [10:54] the web is a big place now computers are [10:57] big too you can buy a gigahertz machine [10:59] with two gigabytes of RAM and a gigabyte [11:02] Ethernet card for $1,200 which you know [11:06] now you keep going into this thing right [11:07] but like in nine [11:09] 99 one of the busiest FTP sites cd-rom [11:12] comm actually handled 10,000 clients [11:15] simultaneously like I just love that [11:17] sentence because it really positions you [11:19] in this like late 90s kind of mindset [11:21] right like FTP site not a thing people [11:23] use anymore most of the time CD ROMs and [11:26] cd-rom comm just like really spells out [11:29] 1999 for me I don't know but ultimately [11:33] the point here is that like we need to [11:37] be able to handle as many like as many [11:40] users as possible especially when we're [11:42] building web services and so this is the [11:44] kind of thing that like as the internet [11:45] grew became more and more important and [11:48] so we had to start thinking about this [11:49] and so this is kind of where rusts async [11:51] story kind of kicks off even though Rus [11:54] did not exist in 1999 these problems [11:56] have sort of been like happening over [11:58] the last 20 years to some degree and so [12:01] back in those days we have this web [12:02] server called Apache you can still use [12:04] Apache today obviously and I want to [12:05] make a point that is not like this today [12:07] although I think you can still [12:09] technically use it like this but I don't [12:10] know why you would this is Apache with [12:13] its pre fork behavior and basically what [12:15] happens is is it would spawn so you have [12:18] this control process that would listen [12:19] for a connection somebody hits your web [12:21] site and then a connection would come in [12:23] and it would spawn a child process to [12:25] handle that connection and so you'd be [12:28] able to do this with these resources [12:29] this is like very very heavyweight this [12:31] is like using like a whole process to [12:33] handle things and so then you know you [12:35] move into other sort of options but like [12:37] back in the day you know this was like [12:38] fine but it was not like it was to [12:41] resource-intensive [12:42] and so what they did was they moved to [12:43] this worker model so instead of spawning [12:47] a whole process for each incoming [12:48] connection a child process would have [12:50] its own thread pool with its own [12:52] individual threads running inside that [12:54] process and these would be operating [12:56] system like one-to-one threads and they [12:59] would sort of handoff connections to [13:01] these thread pools and this kind of got [13:03] us much farther we were able to do more [13:05] with less resources because we were no [13:07] longer using this sort of like extremely [13:08] heavy data structure to to handle more [13:11] and more traffic on the web okay I'm [13:15] gonna stop talking about the past now [13:16] let's actually talk a little bit about [13:18] rest finally 15 minutes into my [13:20] presentation so rust [13:23] originally built by Mozilla and the idea [13:26] was that it would be enhancing Firefox [13:28] which is an HTTP client not an HTTP [13:30] server and this does mean that we care [13:32] about i/o but the situation is very [13:34] different like your particular instance [13:36] of Firefox is not handling 10,000 [13:38] requests at the same time or a million [13:41] requests it's the one that's like one of [13:43] those making those requests to a server [13:45] and so having efficient IO was important [13:48] because you know you do need to download [13:50] web pages faster but figuring out [13:52] immediately how to handle massive scale [13:54] was not necessarily an extreme priority [13:56] because it was primarily gonna be [13:58] operating in this client capacity so [14:01] this is some screenshots of the old [14:03] documentation for rust rust originally [14:06] had this module called green and this [14:08] was a green scheduling library with [14:11] green threads as I pointed out m2n [14:13] original rust was like very influenced [14:16] by Erlang and actually looked much more [14:18] similar to go today than it then rust [14:21] actually does like they were much more [14:23] similar way back in the day and so West [14:25] was like okay we're just going to give [14:27] you these green threads because the idea [14:29] is if you want to be efficient of this [14:30] at all you spin up a zillion green [14:32] threads and you handle them for [14:33] connection and that's like totally fine [14:36] they also had this net library that [14:40] existed and so this was synchronous but [14:44] non-blocking network i/o and for some [14:46] reason it repeats twice I don't know [14:48] what this old bug was in the docs if you [14:49] see this module it says the same thing [14:51] two times I don't know anyway you were [14:54] able to do very basic like TCP and UDP [14:56] things in the standard library and it [14:59] was all non-blocking [15:00] and therefore also synchronous which is [15:03] weird like wait a minute I thought the [15:05] whole point of like async was to be non [15:07] blocking and the synchronous would not [15:09] be blocking turns out there's yet [15:11] another thing we didn't talk about in [15:12] this space so many things like isn't [15:14] this a contradiction at all actually [15:16] it's not there's sort of this graph [15:19] where there's only three out of the four [15:20] things so you can have these two [15:22] dichotomies of synchronous and [15:23] asynchronous i/o and blocking or non [15:25] blocking i/o and they're actually [15:27] technically different so synchronous [15:30] blocking i/o is like what the original [15:33] Ruby interpreter did and what the [15:34] original everything interpreter did [15:36] basically [15:36] which is you write your code in a way [15:38] that looks like it blocks and every time [15:39] you call an i/o function it blocks [15:41] everything this is very slow but it [15:43] worked really well for a while [15:44] asynchronous and blocking doesn't really [15:46] make any sense because why are you [15:48] writing your code in a way that enables [15:49] you to not block and then block on it so [15:51] I don't know any languages that actually [15:52] does this because it's like sort of [15:53] nonsense but there's actually a [15:56] difference in non-blocking i/o with [15:58] synchronous and asynchronous versions of [16:01] this thing and so go and Ruby today [16:03] actually offer a synchronous [16:05] non-blocking i/o and what that means is [16:07] when you write go code you're not [16:09] writing in a way that looks async you're [16:11] writing in a way that looks like it goes [16:12] through step by step by step but inside [16:15] in the runtime it actually transparently [16:17] does non-blocking i/o and it will pause [16:19] your program there whereas like if you [16:21] look at nodejs it's asynchronous and non [16:23] blocking so you write all your code in [16:25] an async manner but it's also non [16:27] blocking and so like this also there's a [16:31] ton of options in this space and there's [16:32] different reasons why different [16:33] languages pick things in this trade-off [16:35] and so like we for rust had to figure [16:37] out like which one of these things makes [16:38] any sense originally we picked a [16:40] synchronous and non blocking the [16:44] advantage of synchronous and blocking is [16:45] like it's very straightforward but it's [16:47] like not performant at all so that was [16:48] never really gonna be an option [16:49] asynchronous and non blocking is nice [16:52] because you don't have to write your [16:53] code in a different way you just kind of [16:55] write your code the way that you would [16:56] normally write it but you get some [16:57] performance benefits because under the [16:59] hood the runtime does you know normal [17:02] things and so this is also really common [17:06] in languages that start off with [17:08] synchronous and blocking i/o and they [17:10] want more performance because it doesn't [17:11] require your code to change in order to [17:13] get faster and so you can sort of like [17:15] do it later so I mentioned Ruby because [17:17] I was familiar with Ruby beforehand it [17:19] was kind of the same thing like [17:20] eventually they added non-blocking i/o [17:21] internally no one's Ruby programs needed [17:23] to change everything just kind of got [17:25] faster asynchronous non-blocking your [17:29] code looks like it doesn't block and it [17:31] doesn't block but it's harder to write [17:32] because you have to deal with a [17:34] synchronicity in some in some fashion so [17:38] we had this blocking not like blocking [17:42] but asynchronous i/o in rust but [17:44] unfortunately that wasn't good enough [17:47] see this is what the rest website used [17:50] to look like [17:50] time ago and it describes rust as a [17:53] systems programming language that runs [17:54] blazingly fast prevents all crashes and [17:57] eliminates data races but when I told [17:59] you before about like green threads [18:01] versus system threads like green threads [18:03] are provided by your runtime and native [18:05] threads are provided by your OS like [18:07] what does it mean to be a systems [18:08] programming language if you don't offer [18:10] your users the ability to use system [18:12] threads like it's like a fundamental API [18:15] of the OS but we're not gonna let you [18:16] use it that seemed really like [18:18] incoherent and so we didn't think that [18:20] like blocking green threads was really [18:23] like the answer for rust ultimately and [18:26] this became like one of the first big [18:27] battles in rust design there were [18:30] actually people like threatened to fork [18:31] the language over the situation and like [18:34] what ended up happening was you'll see a [18:36] couple releases later the docs are a [18:37] little fancier now we provided this sort [18:40] of native package that would give you [18:41] one to one [18:42] threading as long as alongside the green [18:45] package that would give you the m10 [18:47] threading and so the idea was that you [18:49] would get to choose what thing was right [18:50] for your use case you'd write some code [18:52] and it would just like they're both [18:54] threads right so we could just offer an [18:55] API where sometimes you use green [18:58] threads if you want it and sometimes you [18:59] native native threads if you want it and [19:01] everything like should be totally good [19:02] and dandy right you get all the options [19:03] turns out that like doesn't actually [19:06] really work and so we had this whole [19:09] thing this is a bunch of text I'm not [19:11] gonna read you but basically like you [19:12] write your code against this sort of [19:14] runtime interface and you would pick [19:16] which one that that was also not good [19:20] the battles continued people still [19:22] threatened to like actually for crusts [19:24] someone did and then like five people [19:26] favorite on github and nobody used the [19:27] fork anyway but like it was a serious [19:30] problem and like you know we weren't [19:31] like happy with this situation and part [19:34] of the reason why is so this is like an [19:36] example of an RFC where we eventually [19:39] decided to remove the runtime so rest of [19:41] this open design process where you can [19:43] submit improvements for the language and [19:44] this is RFC 230 which is the one that [19:46] actually removed green threads entirely [19:49] and so what we decided was like we [19:54] offering both doesn't actually work and [19:56] the reason it didn't work was that in [19:58] order to do the abstraction to put above [20:01] both of them you added so much [20:03] bookkeeping to the green [20:04] that they were not actually very [20:05] lightweight anymore they were just kind [20:07] of a crappier version of one-to-one [20:08] threads so like they weren't actually [20:10] faster in performance and there's no [20:11] reason you'd pick them and like if you [20:14] gave people the ability to sort of use [20:16] operating system threads and green [20:18] threads it just got way too complicated [20:20] and like it was just bad [20:22] and so with 1.0 kind of like looming we [20:26] had to decide like we're gonna be [20:28] freezing an interface sort of for all [20:30] time so like what do we actually do here [20:32] because this clear that the situation is [20:34] not working and it's clear the other [20:35] situation was not working so we decided [20:39] operating system threads are part of the [20:41] operating system [20:42] we're systems language those things work [20:44] it's fine let's just do it [20:46] so rust 1.0 shipped with a very minimal [20:49] like straightforward operating system [20:51] threads everything is like blocking and [20:54] it's just OS threads and you don't get [20:56] anything fancy and so that was great and [21:00] it got us it let us get rust won't point [21:02] out the door and that was awesome this [21:04] was in May of 2015 so you can see this [21:07] story like started sort of in like 2011 [21:09] 2012 we're up to 2015 now we still got [21:12] four years to go [21:13] right 15:19 yeah so uh the rest 1.0 [21:18] happened but like still everything [21:20] wasn't actually totally great see people [21:23] really loved rust and that was really [21:25] awesome but that meant that they used it [21:27] more and when people started using [21:29] things more they start wanting more [21:30] stuff from you.if rest had faded into [21:33] obscurity we probably were just sort of [21:34] stuck with the only thread like the [21:35] thread api and that would have been [21:36] totally it and that would've been fine [21:37] but it turns out that people wanted to [21:39] build more and more stuff and [21:40] specifically they started to really want [21:43] to be able to build network services in [21:45] rust so this is where that history with [21:46] apache comes crashing back in and that [21:48] like people actually didn't want to just [21:51] build clients and firefox and stuff like [21:53] that [21:54] they actually wanted to be able to build [21:55] network services to do stuff and so they [21:59] started building them with the existing [22:01] api's and that led us to like like so [22:05] russ is supposed to be a [22:06] high-performance programming language [22:08] but like the IO IO model feels like it's [22:11] out of 1999 like old-school Apache like [22:14] spin up all these big heavyweight [22:15] threads and [22:17] it definitely wasn't performing as well [22:19] as people want it to be even though rust [22:21] is like supposed to be a super high [22:23] performance language and like how could [22:24] we claim that like we're as fast as C [22:26] well just not literally being as fast as [22:28] C because IO is like a problem [22:30] so this eventually led us to have to [22:33] come up with a totally different [22:34] solution as well and so the real problem [22:38] here is that native threads are a bad [22:40] fit for IO and that's because of one [22:43] word economy CPU bound versus i/o bound [22:45] snuck one more definition in here I [22:46] think there might actually be two or [22:48] three more but so these are like ways to [22:52] characterize different kinds of [22:53] computations so a CPU bound computation [22:55] is when your processor is doing like a [22:56] lot of active work so how fast you can [22:59] do this work depends on how fast your [23:01] CPU is able to crunch all those numbers [23:03] and get that work done [23:05] whereas IO bound happens when you're [23:07] doing a lot of networking and that often [23:09] means that the speed of completing the [23:10] task is based on doing a lot of i/o and [23:13] the problem with IO is you're often [23:15] waiting on other people so if I'm making [23:17] a request to a server somewhere it [23:19] doesn't matter if I have like the [23:20] fastest CPU that exists or a 486 if it [23:22] takes the web server one second to [23:24] respond I'm gonna be waiting one second [23:26] because like it's not about my CPU it's [23:28] about the other thing in the system and [23:30] so that's like the real problem here is [23:33] that when you're doing a lot of i/o [23:34] you're actually mostly just waiting [23:36] around for other people you're not [23:37] making use of your CPUs resources to do [23:40] that kind of stuff you're just like [23:41] waiting and doing nothing this is why it [23:44] scales actually because it turns out [23:46] that the fastest thing to do is nothing [23:47] at all [23:48] so if you have like a ton of people that [23:49] are all trying to like access your [23:51] website at the same time be are waiting [23:53] on most of them you could handle even [23:55] more people because you're not actively [23:56] doing work for most of your visitors and [23:58] this is why a synchronous stuff really [24:00] scales up well when you're doing i/o and [24:02] turns out that parallelism is generally [24:04] better for things that are CPU bounds so [24:06] you kind of stew don't want both but [24:07] like it doesn't like native threads in [24:10] that whole model like don't really work [24:11] very well in this situation because when [24:14] you're doing a lot of waiting if you [24:15] make a thread for every single incoming [24:16] connection you're tying up all the [24:18] system resources and literally do [24:20] nothing with them so like it doesn't [24:22] work so some other programming languages [24:25] have come along that have solved these [24:26] problems I'm using go here because it's [24:28] popular and people familiar with it [24:30] Erlang does the same thing go kind of [24:31] took this from Erlang and you may [24:33] recognize this from the earlier part of [24:34] Russ steel so it has asynchronous i/o [24:37] but blocking with green threads so what [24:39] happens is you write your code and when [24:41] you do an i/o call the the runtime [24:43] itself will sort of sleep and go [24:46] routines you can map a bunch of go [24:47] routines onto a single operating system [24:49] thread and life is good like you get [24:51] higher performance this is totally like [24:53] a super battle-tested well-worn way to [24:55] do this kind of thing [24:57] however previously in this presentation [25:00] you may remember this slide and green [25:03] threads do have a big big disadvantage [25:04] and that's when you call in to C code [25:07] it's slower this is totally fine for go [25:10] because go is trying to like put [25:12] everything you go and that's also [25:14] totally fine for Erlang because like [25:15] it's not that big of a deal you're not [25:16] using Erlang to crunch numbers but rust [25:18] is supposed to be a systems programming [25:20] language so like if we did green threads [25:22] what would a systems programming [25:24] language that's more expensive to call [25:25] in to see an operating system code like [25:27] mean like that's also incoherent so we [25:29] couldn't really copy this model and I [25:31] want to emphasize it that's not because [25:32] doing the other thing is like the wrong [25:34] thing it just turns out that when you're [25:36] building different kinds of languages [25:37] you have different constraints and so [25:39] our constraints are that we had no [25:41] overhead and calling into C we designed [25:42] the language itself around this kind of [25:44] thing so if we had to do a FRA oh that [25:46] would be terrible and so this is also [25:48] like part of why we got rid of the green [25:49] green threading API that existed a long [25:51] time ago too this is like a second [25:53] reason that didn't work but also sort of [25:54] why we couldn't really return to it [25:57] luckily there's actually another way [26:01] there's this other web server that [26:02] became popular eventually and that's [26:04] called nginx and nginx did asynchronous [26:07] IO with this thing called an event loop [26:08] there was no event loop picture so I did [26:10] draw my own I don't know if I did a good [26:12] job or not whatever and it flipped the [26:13] arrows around but the idea here is the [26:16] thing called event at i/o where you sort [26:18] of have these events and you register [26:20] handlers for each event and then it [26:22] fires off this handler whenever the [26:24] event actually occurs and this means you [26:26] get really good performance but you also [26:28] don't need multiple threads because the [26:30] event loop is one single thread that is [26:32] handling running all these things most [26:34] of the things are sleeping so it's [26:35] totally fine you're not spending up new [26:37] threads for them and it all just kind of [26:39] works this guy named Ryan Dahl who back [26:42] in 2009 [26:44] the assistant called nodejs you may have [26:46] heard of it and it's a little hard to [26:48] read in this wonderful screenshot of [26:50] this terrible hand camp this is the [26:52] original like Jess coffee you so it was [26:55] the first one and it was 2009 so there [26:57] ought a V set up wasn't that great and [26:58] you upload that to YouTube and then you [27:00] screenshot it and you paste it into a [27:01] presentation you get this so I'm sorry [27:03] but uh it says there vented non-blocking [27:06] i/o was really important and that's kind [27:08] of like the whole reason it's the third [27:10] bullet point like what nodejs is [27:12] server-side JavaScript built on v8 [27:14] evented non-blocking i/o and the rest of [27:16] this presentation goes into why like IO [27:18] and handling IO is like the reason that [27:20] node existed and Ryan cared a lot about [27:22] this a lot and so like enabling this [27:25] kind of thing in a programming language [27:26] was important to him so the issue of [27:30] invented IO is that it really requires [27:31] like non-blocking api is because when [27:33] you combine blocking code and non [27:35] locking code together things get awkward [27:36] but that's a whole separate talk Allah [27:37] cannot get into but fundamentally like [27:40] the way this works in node is if you [27:42] have a blocking API like this top one [27:44] you call filesystem read file sync and [27:46] this blocks until the file is read and [27:48] comes into data but since we need to [27:50] like do this asynchronously and we're [27:52] registering those events so the event [27:53] loop can process it this means you have [27:55] callbacks so here's the equivalent [27:57] example asynchronous you pass a closure [28:00] into read file and you check things [28:03] inside of there and so this this closure [28:04] happens this is like a minor [28:06] transformation it looks totally fine the [28:08] problem comes when you start building [28:09] bigger and bigger and bigger things you [28:12] get this callback hell Turner that the [28:14] website callback hell calm this is an [28:16] example that they have of writing [28:18] JavaScript in this style basically you [28:21] end up there's also this amazing image [28:22] and put in the presentation but somebody [28:23] made Rio giving a Hadouken like to like [28:26] click this thing to the pushing [28:28] everything over I don't know if any [28:29] plays video games here but the point is [28:32] is that like when you start writing this [28:33] callback stuff stuff start drifting [28:35] right and you can't really like do a [28:37] whole lot about it it's sort of awkward [28:39] but javascript people are crafty and [28:42] they came up with this thing called [28:42] promises so promise is like an object [28:46] that gives you a value that's not [28:48] necessarily known when the promise is [28:49] built the definition says and so this [28:52] lets a promise is either pending which [28:54] means that the computation isn't [28:55] finished yet [28:56] it's [28:57] filled meaning either succeeded or it [28:59] was rejected meaning operation failed [29:01] this is fine [29:03] this lets you take code that looks like [29:06] this instead so here's an example from [29:08] MDN where you create a promise and then [29:11] after a certain period of time 250 [29:13] milliseconds you resolve it with success [29:14] and so then instead of like chaining [29:17] nested callbacks you can then say my [29:18] promise dot then and do another thing [29:21] and so we've removed that right word [29:22] drift because now we just chained then [29:24] in line so now we have a zillion [29:26] closures vertically instead of a zillion [29:28] closures indented the whole way [29:30] which is better admittedly but still not [29:33] great for another couple reasons like [29:35] Ben then and then then the folks over at [29:41] Twitter also we're dealing with scale [29:42] they had originally built their stuff in [29:44] Ruby on Rails and it was not scaling in [29:46] part because of i/o issues and so they [29:49] ended up rewriting everything on the JVM [29:50] and being really invested in Scala and [29:52] so there was this paper called your [29:54] serve as a function that came out I was [29:56] talking about how they built high [29:57] performance network services at Twitter [29:59] and it introduces this concept called a [30:02] future among other things and a future [30:05] is sort of like a promise but different [30:07] it's a little tough basically like they [30:10] represent the exact same ideas but they [30:12] have a different API I'm not gonna get [30:14] into that at this particular moment but [30:16] we saw this happening in the Scala world [30:17] and we were like aha this seems awesome [30:20] and we know that like callbacks are not [30:22] great because we've learned from node [30:23] and promises are better and futures are [30:25] sort of like a different version of [30:26] promise and they seem to fit a little [30:27] bit better Scala is also statically [30:30] typed we're statically typed like let's [30:32] do what they're doing and so um in [30:34] August of 2016 we announced zero cost [30:36] futures and rust and so we wanted the [30:40] ability to be able to do this sort of [30:41] asynchronous programming in a better way [30:43] you notice that date there 2016 still [30:46] churning along so here's what futures [30:49] look like in 0.1 in 2016 so you have [30:52] this trait called future and I don't [30:54] know if all of you arrest programmers or [30:56] not but basically a trait is like an [30:57] interface so this interface has two [30:58] required types an item type and error [31:00] type and there's this function called [31:02] pol which you would call it and it would [31:05] return either the item or an error [31:07] there's also a ton of other convenience [31:09] functions on here [31:10] like and then as you get this code at [31:12] the bottom that looks very similar to [31:13] that promise code except for says and [31:15] then instead of then cuz you know can't [31:18] name everything the same way across [31:19] languages that would be too hard but you [31:22] get the same idea of like okay we can [31:24] write these things and this is an [31:25] asynchronous computation that processes [31:28] these things in this order and so you [31:29] build up these giant chains of futures [31:31] and then that chain of futures becomes a [31:34] task you submit it to the asynchronous [31:35] system and it works and things were [31:38] totally fine as I mentioned promises and [31:40] features a little different one of the [31:41] other major features other than [31:43] interface aspect to promises in futures [31:44] is that promises are built into the [31:46] JavaScript language and JavaScript is a [31:48] language has a run time which means [31:50] among other things that a promise can [31:53] start executing when you make it because [31:55] it's a construct the language knows and [31:56] it's able to spin stuff up in the [31:58] background because JavaScript assumes [31:59] that everything is asynchronous and so [32:01] nothing happens the order they expect [32:02] and so this is simpler in a lot of ways [32:05] than the other way I'm about to describe [32:06] to you but one of the problems is is if [32:08] you start executing your thing [32:09] immediately you basically need an [32:11] allocation for every single thing you've [32:13] chained on because you don't know at the [32:14] time that you're building the thing like [32:16] it's already running before you finish [32:17] like putting it together even in some [32:19] cases and so so it works but it's it's [32:23] not super efficient and so futures on [32:26] the other hand are not built into rust [32:27] the language they're an entirely a [32:28] library because again we can't have a [32:30] runtime cannibal that's built in stuff [32:32] because we need to be as low-level as C [32:34] so like we can't afford to just do that [32:37] and that also means that unlike promises [32:39] in JavaScript you build up this chain of [32:41] futures and nothing actually happens [32:43] with them at all until you explicitly [32:45] hand them over to an executor object and [32:47] say please run this for me in the [32:48] background and so they don't do anything [32:50] until the pull method is called and so [32:52] this is a little more complicated but [32:54] it's also extremely efficient because [32:56] you know how big the task is going to be [32:58] when you create it and so unlike green [33:01] threads where you make a small thing and [33:02] then when it grows too big you have to [33:03] reallocate and do all that other stuff [33:04] we can know at compile time exactly how [33:07] big a task needs to be and allocate [33:09] exactly the right amount of memory and [33:10] never grow or shrink that stack and you [33:12] will always use a perfect exact sized [33:14] allocation which is really cool it also [33:17] means you only have one allocation and [33:19] you can actually do this if you're [33:20] willing to do it like put certain limits [33:21] on it you can actually do it [33:22] dynamic allocation you can do it [33:24] statically Bawa but like this is super [33:27] hyper efficient and also because we have [33:29] compiler and compilers are awesome and [33:31] we do everything at compile time [33:33] the futures actually compiled down into [33:35] the like perfect state machine that you [33:38] would write by hand if you were doing [33:39] the event at i/o yourself so it becomes [33:41] an extremely efficient way to to do this [33:45] kind of asynchronous computation but it [33:47] also is easier to write because it has [33:50] this like it's not a bunch of nested [33:51] callbacks so here's the example of using [33:54] futures 0.1 this is using a package [33:56] called Tokyo which is a executor that [33:58] exists so you have an IP address and you [34:01] create a listener and then for listener [34:03] incoming is the thing that returns a [34:05] sequence of futures in a row so you on [34:07] each of those you do nothing and then if [34:10] there's an error you print it out so [34:11] this is like just doesn't do anything [34:12] but just accepts connections and then at [34:14] the bottom here you see Tokyo run you're [34:16] passing that server like this chain of [34:18] futures into the executor and it [34:21] executes so that's kind of like the [34:23] model the way that it worked in 2015 and [34:26] so we need to know if this is good or [34:28] not so we built a bunch of stuff on top [34:29] of it and that was really cool but it [34:32] also had some problems so there was a [34:36] thing called future zero point two and [34:38] one of the big problems with zero point [34:40] one is zero point two you can see the [34:41] difference here there's this context [34:43] argument a task context and so future [34:45] zero point one relied on thread-local [34:46] storage to sort of like deal with some [34:48] bookkeeping around the future package [34:50] and that minute relied on thread-local [34:51] storage which means you couldn't use it [34:53] on things like bare metal and so that [34:55] became sort of a problem and so the [34:57] intention was what if we made this [34:58] context explicit and that would help [35:00] alleviate this situation and that was ok [35:03] but like also we knew it wasn't perfect [35:06] and we're gonna do more stuff and so [35:08] someone's like hey do you suggest that [35:10] the ecosystem should break between one [35:12] and two or like should we wait for three [35:15] and Aaron who are this blogpost said you [35:17] shouldn't you shouldn't move everything [35:18] over to is a good enough snapshot but [35:21] we're gonna have three coming out in a [35:22] couple of months or less and then people [35:24] are like that seems a little bad cuz [35:26] like you need to try it out in order to [35:28] use it but your libraries update how are [35:30] you supposed to try it out and then if [35:31] it's gonna change again why would anyone [35:33] try it out in the first place [35:34] this person was smart but [35:36] cuz as you can see one year ago we said [35:38] it was should become in a couple months [35:40] so that sort of been a situation so [35:47] going back to JavaScript for a moment [35:49] async/await so here's three examples of [35:52] writing different code we have the [35:53] original callback kind do you make a [35:54] request you pass in a callback [35:56] then there's promises where it's similar [35:58] but you chain then it looks the same in [36:00] this situation but it avoids that right [36:01] hand adrift I showed you before but [36:03] finally c-sharp actually originally [36:06] conceived of this feature but since I [36:07] was using JavaScript earlier I decided [36:09] to keep it in JavaScript here you say [36:11] you have an async function instead and [36:13] then you write a wait request and so [36:16] this actually does a sort of [36:18] transformation to be able to do the same [36:20] thing as the code above be good to write [36:21] it in a way that looks like it's [36:23] blocking it goes back to like writing [36:25] code and in normal fashions that have a [36:27] weird nested chain of things so yeah [36:31] c-sharp actually invented this first [36:32] javascript was like oh this will save us [36:34] from a million promises being terrible [36:36] and life is good over there because it [36:40] lets you write code that looks like a [36:41] synchronous even though it's not the [36:43] other way around [36:44] she's like code that looks like it's not [36:46] synchronous but it actually is [36:47] asynchronous and here's the thing that's [36:50] interesting we didn't actually realize [36:51] until we played with future zero point [36:53] two even more but a single weight is [36:55] actually more significant to rust than [36:57] in other languages so in c-sharp in [36:58] JavaScript is mostly a convenience [37:00] feature because like writing promises by [37:01] hand is like a little terrible but like [37:03] it does work but in rust there's no [37:07] garbage collector and what that means is [37:09] that remember that thing about how in [37:11] the like promised world you create all [37:13] these individual allocated things [37:14] everywhere well like if we did the same [37:16] thing with futures which we sort of [37:18] don't but like you're keeping track of [37:19] this future access is a thing and the [37:21] next features chain and you're dealing [37:22] with all that it turns out that that's [37:24] real hard and rust didn't understand [37:26] that at all and so here's an example of [37:29] like a synchronous API that does read so [37:31] this is a function that takes in itself [37:33] and a buffer and returns how many bytes [37:35] are read or an error and so it would [37:38] look like this you have the buffer area [37:39] of a cursor you read in 124 bit chunks [37:42] our byte chunks and you put stuff in the [37:44] cursor and this is like very [37:46] straightforward synchronous API so how [37:48] we turn this into futures like we have [37:50] this great design for futures how do we [37:52] replace this synchronous version with [37:53] the async version well the type [37:56] signature turns into this instead and it [37:59] turns out that all the code you need is [38:00] actually too big to fit on this slide so [38:01] I just didn't even include it and the [38:03] reason why is because once you start [38:05] chaining these futures together the rust [38:07] compiler doesn't understand how you're [38:09] using memory anymore and so it gives you [38:11] sort of like the most restrictive thing [38:12] possible which means that like you just [38:15] your life is not good it actually is [38:18] really terrible and the reason of this [38:20] is due to that again like you create the [38:23] thing first and then you give it to the [38:25] executor and then it executes and so the [38:27] constraints on like checking that all [38:29] the memory works the right way is [38:31] different when you make that future then [38:33] when you actually hand it off to be [38:34] executed and that that change is the [38:37] thing that the compiler just like has no [38:39] idea how to handle whatsoever and so [38:40] this meant we needed to choose more [38:42] allocations we need to do more in run [38:44] time checking for a thing that like does [38:46] not actually need it but just to satisfy [38:48] the compiler and so then this in a [38:50] similar way to like the green thread [38:51] versus native abstraction where the [38:53] green threads got to be too expensive [38:55] this super hyper efficient thing didn't [38:58] actually do that for any real-world code [38:59] it turned out the code you had to write [39:01] to use the hyper efficient thing was [39:02] very inefficient so we're now back to a [39:04] place where like this is not fast and [39:05] it's hard to write so life is terrible [39:07] but a single wait to the rescue so with [39:11] a single wait the compiler and the [39:13] borrow checker can understand the way [39:16] that these things interact with each [39:17] other and so your life becomes good [39:19] again and so here's that same API but [39:21] with async await you notice the only [39:23] difference is like an async block [39:24] wrapped around it and a dot wait there [39:26] and otherwise it looks exactly the same [39:27] as the original blocking code so we've [39:30] now solved these sort of ergonomic [39:32] issues through a single weight but there [39:35] were other problems they're always more [39:36] problems it turns out that not all [39:38] features can error because we had [39:40] conceived of futures as being an i/o [39:42] thing we put error into the futures [39:44] itself but turns out that it's not just [39:45] an i/o thing because once you start [39:47] using futures they infect your entire [39:49] code base and not all the things that [39:50] you're doing are things that can [39:51] actually fail and so this means you add [39:54] a ton of boilerplate for things that are [39:55] infallible which like seems terrible [39:57] because you're making things that are [39:58] easy more complicated than they need to [40:00] be [40:00] and so the final design that we landed [40:04] on for futures and the one that is in [40:06] the standard library soon is called [40:08] standard future and it now holds a [40:10] single output type and the signature has [40:12] changed and a bunch of other interesting [40:13] ways there's this pin thing I'm good see [40:15] you later and we still have our context [40:17] but now we just returned the output type [40:19] and so pin is part of how async/await [40:21] teaches the borrow check or how the code [40:23] works I'm not going to get into it [40:24] because frankly I can give you a 45 [40:26] minute talk just on pain itself it's [40:27] complicated it doesn't matter virtually [40:30] no one the only people who ever interact [40:31] with this particular part of the API are [40:33] people who are writing low-level [40:34] libraries and rust and you as a user [40:36] could not think about it so I'm just [40:37] gonna ignore that exists other than to [40:40] say that that's what taught the borrow [40:41] check are how to do these things and [40:43] then finally if you need a future that [40:45] has an error you just compose our [40:46] regular old result type as the output [40:48] type and so future is that don't have [40:50] errors or fine features that do have [40:52] areas still work life is good right no [40:55] it turns out there's more more problems [40:59] what syntax to use for async await you [41:02] would think this is a simple question [41:03] but it turns out that when you're [41:04] designing a programming language you [41:05] have to think about edge cases and it [41:07] turns out there's a lot of edge cases [41:08] they're specific to rust the old [41:10] languages didn't have to deal with so [41:12] Java and c-sharp both do a weight space [41:14] value nice and straightforward [41:16] everything's everything's fine but we [41:19] have this question mark thing for error [41:21] handling and you're gonna use it a lot [41:23] because async IO has errors like si out [41:25] that happens so what happens if you were [41:28] to copy that syntax but you need to use [41:30] the question mark for error handling [41:31] like does a weight value question mark [41:33] where it's parentheses like this or like [41:36] this [41:37] you would when you're writing this what [41:39] you want is almost always the second one [41:42] but the syntax kind of implies the first [41:44] one so it was like should we change the [41:47] precedent of the question mark operator [41:49] but only when a weight exists and people [41:51] were like that seems not like a good [41:53] idea [41:54] other people were like that's the only [41:56] possible value and so we did what most [41:59] good programmers do in the situation oh [42:01] yeah I forgot one more thing [42:03] it's like alpha load also chaining start [42:05] chaining this with spaces this gets [42:07] nasty really quickly and this still [42:08] happens in JavaScript and sometimes in [42:10] my understanding I'm a super big [42:11] JavaScript person but [42:13] anyway we did what everyone does on the [42:15] Internet we argued and we argued and we [42:17] argued and we argued and then we argued [42:20] some more people proposed like every [42:23] possible syntax for this there was like [42:25] people are saying like maybe we should [42:27] use an upside-down question mark for the [42:28] one that has the different precedents [42:29] like that was an actual like there was [42:31] people every possible aspect of this [42:34] design was like fought over I mean like [42:36] it's not a joke like this is multiple [42:39] threads on our discourse instance 478 on [42:42] one of those 12 the other one somebody [42:44] else posted the screenshot of their [42:46] email INBOX [42:47] and was like you can see those numbers [42:48] they're like 25 35 160 seven eight [42:51] almost all these are about a single [42:53] weight all individual threads the reddit [42:56] had like these are just the top two [42:57] there were actually more threads than [42:59] this but like 160 833 people really like [43:02] talking about syntax as it turns out and [43:04] so the problem was is that everybody [43:07] hated all the options that were not [43:08] their favorite but there was no clear [43:10] winner because that's just what happens [43:11] a lot of times its syntax everything has [43:12] drawbacks [43:13] everything has positives so at some [43:15] point we decided like hey we can't let [43:18] this incredibly important feature to [43:19] rust just be stuck down in the syntax [43:21] decisions and so we picked one by we I [43:24] mean other people I didn't have to do [43:25] that thank God [43:26] so he's noticed we have this dot await [43:28] here you may have noticed that it was [43:30] different than the the c-sharp thing so [43:31] with no errors you call just dot await [43:33] no parentheses because it's not a method [43:35] call it's a key word and with errors you [43:37] just add the question mark everything [43:39] looks great [43:40] some people really viscerally hate this [43:42] other people or like it seems weird at [43:45] first and then 15 minutes later I forgot [43:46] about it and it's fine I think that in [43:48] the end that's really how it's gonna [43:49] happen but uh we ended up with a weird [43:51] wait syntax it just happens it's the [43:53] best choice for rust I think in the end [43:55] it was not my preferred option but I [43:57] just want to be able to use the feature [43:58] so at some point you just gotta like [44:01] yeah but I think it's good after using [44:04] it for a while so it turns out that's [44:07] actually not even the last actual issue [44:09] and this one this is getting long and [44:11] there's too many issues so I'm not even [44:13] again into that one because this talk is [44:15] long enough as it is but like there's [44:16] just always more things that happen this [44:18] one is extremely minor so that's why I'm [44:20] not talking about it but there's other [44:24] stuff we're doing with async/await as [44:25] well [44:26] that have additional like organ Amma [44:28] comprar this is an example of a crate [44:30] called run time and the main thing you [44:31] can see here this is a UDP example that [44:35] just repeats what you say to it but [44:37] there's this runtime main annotation on [44:39] the main function so you know how it was [44:41] like oh you have to submit your futures [44:42] to an executor like turns out this is a [44:44] macro that does that for you so you get [44:46] code that sort of looks like the node [44:47] example where you don't submit anything [44:48] but it turns out it rewrites the code so [44:51] that you do so that's kind of cool and [44:53] so we end up looking like more and more [44:55] similar to to a node this is no syntax [44:57] highlighting so it gets a little messy [44:58] but it looks better in an editor one [45:02] really wild thing is that you can use a [45:03] single weight in webassembly with rust [45:05] so this is an example of a program that [45:09] I wrote recently async/await [45:10] and it fetches some information from a [45:12] database and then there's some actual [45:14] processing here I chopped out but what [45:16] it does is it returns it as part of the [45:18] response so you notice here that is [45:19] wisdom entry file it returns a promise [45:22] and that's a JavaScript promise bound to [45:24] rust so what happens is this call to get [45:29] in here data get that is a call to a [45:32] database that returns a promise a [45:33] JavaScript promise because it's a [45:35] JavaScript API to the database and you [45:37] need to do stuff with it so then you [45:39] turn into a future with J's future from [45:41] and this is a specific kind of future [45:43] that knows how to wrap a JavaScript [45:44] promise and turn into a rest future then [45:46] you call a wait on it and and then at [45:49] the end there's this future to promise [45:51] that we'll take any future you have and [45:53] convert it back into a JavaScript [45:54] promise so yes this code and wasm is a [45:57] JavaScript promise wrapped up in a rasp [45:59] rust future wrapped up in a JavaScript [46:01] promise and then returned through rusts [46:03] back into webassembly back into [46:05] JavaScript and this works it's totally [46:08] fine I mean depending your definition of [46:11] fine I guess but like it is a thing that [46:14] if you go to my website right now this [46:16] code will execute and it's a thing so [46:19] you can do all sorts of fun stuff with [46:20] the compiler is the great so as I wrap [46:24] up with this this long and tortured [46:26] story about all these different options [46:27] now we went through like every single [46:29] possible thing that could have existed [46:30] and figured out that all the more [46:32] terrible until our current situation [46:33] this is finally rant landing in rust [46:35] 1.37 or maybe rust 1.38 [46:37] depending on that last little tiny issue [46:39] that [46:40] about which is it's fine however I wrote [46:44] this slide and this morning I woke up [46:46] and I was gonna go to the conference and [46:47] I looked at Twitter and I think this is [46:50] actually wrong now it it turns out that [46:53] earlier this morning boats tweeted like [46:55] we are actually stabilizing async/await [46:57] for real and that means that it will [47:00] land in rust 138 for sure are finally [47:02] all that little tiny thing is not [47:04] doesn't matter we figured it out it's [47:05] cool [47:06] so this rust 1.38 is landing in I think [47:10] it's the 15th of August so roughly in [47:12] August but like we did it years later [47:15] you know sort of in some sense starting [47:18] the late 90s but that's before rust like [47:19] technically starting around like the [47:21] 2010-2011 era 9 8 or 9 years later we [47:26] finally figured out how to do ILO and [47:27] rust efficiently and it works if you [47:30] look at the latest version of the tech [47:31] empower benchmark which is a thing that [47:32] benchmarks different website things this [47:35] is not the latest report but this is [47:37] like a build that happened last night or [47:39] last week act excision rust and you can [47:42] see that it is dominating the benchmarks [47:43] there's a whole bunch of other [47:44] frameworks below there but the next [47:46] closest thing is 62 percent of the speed [47:48] it does and so we're able to get really [47:50] really really high performance out of [47:52] rust network services thanks to like [47:54] spending all this time sorting through [47:56] all these examples and working through [47:57] every possible thing led us to the like [48:00] finest fastest thing and end it also is [48:02] reasonably ergonomic to write and so two [48:05] lessons to take you away in my last like [48:07] 30 seconds here turns out that a [48:09] world-class IO system implantation takes [48:11] years can't just like make it happen [48:14] it's like surprise finally different [48:18] languages have different constraints [48:19] this means that you can't really argue [48:20] that like go or Erlang style is better [48:22] or worse than Russ style they have [48:24] different pros and cons in all [48:26] directions and so like just because you [48:29] like a thing in one language and you [48:30] move to a different language does not [48:32] mean that the other language did a [48:34] different way for a wrong reason so we [48:35] couldn't always just reuse things from [48:37] other languages because we had different [48:38] constraints about our designs thank you [48:41] so much that is all I have to say today [48:43] you