1. Why Async is Different in Rust 🟢
1. 为什么 Rust 的 async 和别家不一样 🟢
What you’ll learn:
本章将学到什么:
- Why Rust has no built-in async runtime (and what that means for you)
为什么 Rust 没有内建 async 运行时,以及这件事对实际开发意味着什么- The three key properties: lazy execution, no runtime, zero-cost abstraction
三个关键特征:惰性执行、无内建运行时、零成本抽象- When async is the right tool (and when it’s slower)
什么时候该用 async,什么时候反而会更慢- How Rust’s model compares to C#, Go, Python, and JavaScript
Rust 的模型和 C#、Go、Python、JavaScript 相比到底差在哪
The Fundamental Difference
根本差异
Most languages with async/await hide the machinery. C# has the CLR thread pool. JavaScript has the event loop. Go has goroutines and a scheduler built into the runtime. Python has asyncio.
大多数带 async/await 的语言,都会把背后的 machinery 藏起来。C# 有 CLR 线程池,JavaScript 有 event loop,Go 有 goroutine 和内建调度器,Python 有 asyncio。
Rust has nothing.
Rust 默认什么都不送。
There is no built-in runtime, no thread pool, no event loop. The async keyword is a zero-cost compilation strategy — it transforms your function into a state machine that implements the Future trait. Someone else (an executor) must drive that state machine forward.
Rust 没有内建运行时、没有默认线程池、也没有偷偷躲在背后的事件循环。async 关键字本质上是一种零成本编译策略,它会把函数改写成实现了 Future trait 的状态机。真正推动这个状态机往前跑的,必须是外部执行器,也就是 executor。
Three Key Properties of Rust Async
Rust async 的三个核心特征
graph LR
subgraph "Other Languages"
EAGER["Eager Execution<br/>Task starts immediately"]
BUILTIN["Built-in Runtime<br/>Thread pool included"]
GC["GC-Managed<br/>No lifetime concerns"]
end
subgraph "Rust"
LAZY["Lazy Execution<br/>Nothing happens until polled"]
BYOB["Bring Your Own Runtime<br/>You choose the executor"]
OWNED["Ownership Applies<br/>Lifetimes, Send, Sync matter"]
end
EAGER -. "opposite" .-> LAZY
BUILTIN -. "opposite" .-> BYOB
GC -. "opposite" .-> OWNED
style LAZY fill:#e8f5e8,color:#000
style BYOB fill:#e8f5e8,color:#000
style OWNED fill:#e8f5e8,color:#000
style EAGER fill:#e3f2fd,color:#000
style BUILTIN fill:#e3f2fd,color:#000
style GC fill:#e3f2fd,color:#000
这三点是理解 Rust async 的地基:它是惰性的,不自带调度环境,而且所有权、生命周期、Send、Sync 这些规则会原封不动压到 async 世界里。
如果脑子里还带着 “async 一写,任务就自动跑起来了” 这种别家语言的惯性,到了 Rust 这边很容易一头撞墙。
No Built-In Runtime
没有内建运行时
// This compiles but does NOTHING:
async fn fetch_data() -> String {
"hello".to_string()
}
fn main() {
let future = fetch_data(); // Creates the Future, but doesn't execute it
// future is just a struct sitting on the stack
// No output, no side effects, nothing happens
drop(future); // Silently dropped — work was never started
}
这段代码能编译,但什么也不会发生。fetch_data() 被调用时,只是生成了一个 future 对象,它安安静静躺在栈上,等着别人来 poll。如果没人管它,丢掉就结束了。
这点对于从 C# 或 JavaScript 过来的人尤其容易搞混,因为那边一创建 task 或 promise,通常就已经开跑了。
Compare with C# where Task starts eagerly:
对比 C#,Task 是急切执行的:
// C# — this immediately starts executing:
async Task<string> FetchData() => "hello";
var task = FetchData(); // Already running!
var result = await task; // Just waits for completion
Lazy Futures vs Eager Tasks
惰性 Future 与急切 Task
This is the single most important mental shift:
这是最关键的一次思维切换:
| C# / JavaScript / Python | Go | Rust | |
|---|---|---|---|
| Creation 创建时 | Task starts executing immediatelyTask 会立刻开始执行 | Goroutine starts immediately goroutine 立刻启动 | Future does nothing until polledFuture 在被 poll 前什么都不做 |
| Dropping 被丢弃时 | Detached task continues running 脱离引用后往往还会继续跑 | Goroutine runs until return goroutine 会一直跑到返回 | Dropping a Future cancels it future 一旦被丢弃,就等于取消 |
| Runtime 运行时 | Built into the language/VM 语言或 VM 自带 | Built into the binary (M:N scheduler) 二进制里自带调度器 | You choose (tokio, smol, etc.) 由使用者自己选,例如 Tokio、smol |
| Scheduling 调度 | Automatic (thread pool) 自动调度 | Automatic (GMP scheduler) 自动调度 | Explicit (spawn, block_on)显式触发,例如 spawn、block_on |
| Cancellation 取消 | CancellationToken (cooperative)CancellationToken 协作式取消 | context.Context (cooperative)context.Context 协作式取消 | Drop the future (immediate) 直接丢弃 future |
// To actually RUN a future, you need an executor:
#[tokio::main]
async fn main() {
let result = fetch_data().await; // NOW it executes
println!("{result}");
}
所以 Rust async 里最容易踩空的一点,就是把“创建 future”和“执行 future”混为一谈。前者只是在组装工作单,后者才是真正开始干活。
这也是为什么 Rust 很强调 executor、spawn、block_on 这些词。没有它们,future 只是一个静态对象,不会自己动。
When to Use Async (and When Not To)
什么时候该用 async,什么时候别硬上
graph TD
START["What kind of work?"]
IO["I/O-bound?<br/>(network, files, DB)"]
CPU["CPU-bound?<br/>(computation, parsing)"]
MANY["Many concurrent connections?<br/>(100+)"]
FEW["Few concurrent tasks?<br/>(<10)"]
USE_ASYNC["✅ Use async/await"]
USE_THREADS["✅ Use std::thread or rayon"]
USE_SPAWN_BLOCKING["✅ Use spawn_blocking()"]
MAYBE_SYNC["Consider synchronous code<br/>(simpler, less overhead)"]
START -->|Network, files, DB| IO
START -->|Computation| CPU
IO -->|Yes, many| MANY
IO -->|Just a few| FEW
MANY --> USE_ASYNC
FEW --> MAYBE_SYNC
CPU -->|Parallelize| USE_THREADS
CPU -->|Inside async context| USE_SPAWN_BLOCKING
style USE_ASYNC fill:#c8e6c9,color:#000
style USE_THREADS fill:#c8e6c9,color:#000
style USE_SPAWN_BLOCKING fill:#c8e6c9,color:#000
style MAYBE_SYNC fill:#fff3e0,color:#000
Rule of thumb: Async is for I/O concurrency (doing many things at once while waiting), not CPU parallelism (making one thing faster). If you have 10,000 network connections, async shines. If you’re crunching numbers, use rayon or OS threads.
经验法则: async 适合处理 I/O 并发,也就是“一边等网络、文件、数据库,一边把别的事情先做掉”;它并不是拿来给 CPU 密集计算提速的。如果有 1 万个网络连接,async 很亮眼;如果是在死命算数,优先考虑 rayon 或系统线程。
When Async Can Be Slower
什么时候 async 反而更慢
Async isn’t free. For low-concurrency workloads, synchronous code can outperform async:
async 从来不是免费的。并发量不高时,同步代码完全可能比 async 更快:
| Cost 代价 | Why 原因 |
|---|---|
| State machine overhead 状态机开销 | Each .await adds an enum variant; deeply nested futures produce large, complex state machines每个 .await 都会引入新的状态变体,future 嵌套深了,状态机就会变大变复杂 |
| Dynamic dispatch 动态分发 | Box<dyn Future> adds indirection and kills inliningBox<dyn Future> 会带来额外间接层,还会影响内联 |
| Context switching 上下文切换 | Cooperative scheduling still has cost — the executor must manage a task queue, wakers, and I/O registrations 协作式调度照样有管理成本,执行器要维护任务队列、waker 和 I/O 注册 |
| Compile time 编译时间 | Async code generates more complex types, slowing down compilation async 代码会生成更复杂的类型,编译速度也会跟着受影响 |
| Debuggability 调试可读性 | Stack traces through state machines are harder to read (see Ch. 12) 状态机展开后的调用栈更难看懂,调试体验通常更拧巴 |
Benchmarking guidance: If fewer than ~10 concurrent I/O operations, profile before committing to async. A simple std::thread::spawn per connection scales fine to hundreds of threads on modern Linux.
基准建议: 如果并发 I/O 数量不到十来个,先测再说,别急着全盘 async 化。现代 Linux 上,一连接一线程在几百线程级别都未必是问题。
Exercise: When Would You Use Async?
练习:什么时候会选 async?
🏋️ Exercise 🏋️ 练习
For each scenario, decide whether async is appropriate and explain why:
针对下面几个场景,判断 async 是否合适,并说明原因:
- A web server handling 10,000 concurrent WebSocket connections
1. 一个要同时处理 1 万个 WebSocket 连接的 Web 服务。 - A CLI tool that compresses a single large file
2. 一个压缩单个大文件的命令行工具。 - A service that queries 5 different databases and merges results
3. 一个同时查询 5 个数据库并合并结果的服务。 - A game engine running a physics simulation at 60 FPS
4. 一个以 60 FPS 跑物理模拟的游戏引擎。
🔑 Solution 🔑 参考答案
- Async — I/O-bound with massive concurrency. Each connection spends most time waiting for data. Threads would require 10K stacks.
1. 适合 async:典型 I/O 密集且并发量巨大。每个连接大部分时间都在等数据,线程模型会额外背上 1 万个栈空间。 - Sync/threads — CPU-bound, single task. Async adds overhead with no benefit. Use
rayonfor parallel compression.
2. 更适合同步或线程:这是 CPU 密集、单任务型工作,async 只会多一层开销,没啥收益。真要并行压缩,用rayon更靠谱。 - Async — Five concurrent I/O waits.
tokio::join!runs all five queries simultaneously.
3. 适合 async:这里的核心是多个独立 I/O 等待,可以用tokio::join!并发把五个查询一起推进。 - Sync/threads — CPU-bound, latency-sensitive. Async’s cooperative scheduling could introduce frame jitter.
4. 更适合同步或线程:这是 CPU 密集而且对延迟抖动敏感的场景,协作式调度可能带来帧时间抖动。
Key Takeaways — Why Async is Different
本章要点:Rust async 为什么特别不一样
- Rust futures are lazy — they do nothing until polled by an executor
Rust future 是惰性的,不被执行器poll就什么都不做。- There is no built-in runtime — you choose (or build) your own
Rust 没有内建运行时,要自己选择,必要时甚至可以自己写。- Async is a zero-cost compilation strategy that produces state machines
async 是一种零成本编译策略,本质上会生成状态机。- Async shines for I/O-bound concurrency; for CPU-bound work, use threads or rayon
async 最适合 I/O 并发;CPU 密集工作优先考虑线程或rayon。
See also: Ch 2 — The Future Trait for the trait that makes this all work, Ch 7 — Executors and Runtimes for choosing your runtime
继续阅读: 第 2 章:Future Trait 解释这一切依赖的核心 trait,第 7 章:执行器与运行时 继续讲运行时该怎么选。