很多人用 async Rust 一段时间后,都会有一种矛盾感:
- 表面上:
async/await用起来很顺 - 实际上:一到性能、内存、卡顿、奇怪的 borrow 报错,就开始失控
于是常见的评价是:
“async Rust 太复杂了”
但真正的问题不是“复杂”,而是:
async Rust 没帮你隐藏任何成本。

1️⃣ 一个必须先打破的幻想:async ≠ 并行
在不少语言/框架里,async 被潜移默化地理解成:
“写起来像同步,但跑起来很快”
Rust 不认这个说法。
在 Rust 的语义里:
- async 不是并行
- async 不是多线程
- async 甚至不是调度
async 在 Rust 中只做一件事:
把函数编译成一个可被暂停和恢复的状态机。
仅此而已。
2️⃣ async fn 的真实形态:一个状态机 + 一堆字段
你写下:
async fn fetch() -> Data {
let a = step1().await;
let b = step2(a).await;
step3(b)
}
编译器看到的不是“异步函数”,而是近似于:
enum FetchFuture {
Start,
WaitingStep1(Step1Future),
WaitingStep2(Step2Future),
Done,
}
并且:
- 每个
await= 一个状态边界 await之前活着的变量 = Future 的字段- Future 必须能被反复 poll
👉 这直接决定了 async Rust 的内存和性能特征。
3️⃣ 为什么 async Rust 对“借用”这么严格?
很多人第一次被 async Rust 折磨,通常是这类报错:
borrowed value does not live long enough
cannot borrow across await
这不是编译器刁难你,而是状态机模型的必然结果。
关键事实
await可能暂停当前任务- 暂停意味着:函数栈被“冻结”
- 冻结的状态会被 移动、调度、存放
如果你在 await 前借了一个引用:
let x = &self.field;
foo().await;
use(x);
那就意味着:
- Future 里保存了一个指向
self内部的引用 - Future 还可能被 move
- 引用地址一旦变化,就会悬垂
Rust 选择了最保守、也最安全的策略:
除非你能证明地址稳定,否则禁止。
4️⃣ Pin 出现的真正原因:不是为难你,而是救你
Pin 是 async Rust 最容易被误解的概念之一。
你需要记住一句话:
Pin 不是“不能动”,而是“不能在你不知道的情况下被动”。
为什么 Future 需要 Pin?
因为:
- async Future 可能是自引用结构
- 一旦开始 poll,它的内存地址就必须稳定
- 否则内部保存的引用就会失效
于是 Rust 设计了一个协议:
- 你可以拿到
Pin<&mut T> - 但你不能再 move 这个 T
- 除非 T 明确声明:
Unpin
这是一种显式的安全契约。
5️⃣ async Rust 的性能核心:不是 await,而是“唤醒”
async 的性能瓶颈,几乎从来不在 await 本身。
真正关键的是:
- poll 频率
- wake 次数
- 任务调度开销
- 状态切换成本
一个重要认知转变
async 任务 ≈ 一个被频繁 poll 的小状态机
- poll = “你现在能不能继续?”
- 返回
Pending= “不行,等我被唤醒” - wake = “我现在可能行了”
大量细碎 async 任务 = 大量调度和唤醒。
6️⃣ 为什么“到处 spawn”往往是性能问题的根源?
很多 async 新手喜欢这样写:
tokio::spawn(async move {
do_something().await;
});
感觉很“并发”,很“异步”。
但在 Rust 里,这意味着:
- 分配一个新的 task
- 注册到调度器
- 维护 waker
- 可能跨线程迁移
spawn 不是免费午餐。
经验法则:
- 逻辑上的 async ≠ 物理上的 task
- 能在一个 task 里 await 的,就别拆
- task 是调度单元,不是代码组织工具
7️⃣ async Rust 的内存模型:你写的不是栈,而是“堆状态”
一个非常反直觉但极其重要的事实:
async 函数里的局部变量,几乎都活在堆上。
因为:
- Future 需要跨 await 存活
- 栈不能被暂停
- 所以状态被打包进一个 struct
这意味着:
- 大 struct = 大 Future
- 在 await 前声明的变量,会活到下一次 await
- 不必要的字段会放大内存占用
于是你会看到成熟的 async Rust 代码中:
- 更小的作用域
- 提前
drop - 把大对象放在 await 之后创建
这是为状态机瘦身。
8️⃣ Tokio / async-std 做的,其实是“任务调度工程”
很多人把 Tokio 当成“异步库”。
更准确的说法是:
Tokio 是一个高性能的任务调度器 + IO 反应堆。
它解决的不是:
- 怎么写 async
而是:
- 怎么调度几十万 Future
- 怎么最小化 wake 成本
- 怎么避免线程抖动
- 怎么在 IO 就绪时高效唤醒任务
一旦你理解 async 的本质是状态机,Tokio 的设计就会变得非常合理、甚至不可替代。
9️⃣ async Rust 的“正确心智模型”
如果你只记住这一篇的一句话,那应该是:
async Rust = 显式状态机 + 显式调度成本 + 显式内存模型
它不帮你:
- 隐藏堆分配
- 隐藏生命周期
- 隐藏调度
- 隐藏唤醒成本
但作为回报,它给你:
- 可预测的性能
- 精确的资源控制
- 在极端负载下仍可推理的行为
结语:async Rust 难,是因为它是“系统级 async”
很多语言的 async,是“应用级 async”:
方便、好用、但代价模糊。
Rust 的 async,是“系统级 async”:
- 每一个成本都真实存在
- 每一个抽象都可以被拆穿
- 每一个性能问题都能被定位
这也是为什么:
一旦你真正理解 async Rust,你会发现它反而很诚实。