异步编程基础
本教程共 78 篇 · 第 71 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:理解 async/await 是怎么回事——Future 是惰性任务、
.await是让出线程而非阻塞线程,分清异步和多线程各自的适用场景,并知道 2024 Edition 为 async 带来的小变化。
如果说前面几章的线程是”一个任务占一个工人”,那异步就是”一个工人同时照看很多任务,谁闲着就干谁”。这一章我们进入 Rust 的异步世界,先打基础概念,运行时要等下一章再用 tokio。
1-1 为什么需要异步
并发有几种常见模型:操作系统线程最简单直观,但创建和切换成本高;事件驱动(回调)性能好,却容易陷入”回调地狱”;协程(如 Go)用起来舒服但抽象层次太高。Rust 选了 async/await:性能好、能贴近底层,写法又和同步代码差不多。
最适合异步的场景是 IO 密集型任务:一个 web 服务器同时要伺候成千上万个连接,每个连接大部分时间在等网络数据,真正计算的时间极少。如果给每个连接开一个线程,操作系统线程数很快见顶,上下文切换也拖垮性能。异步的做法是:一个任务在等 IO 时,立刻把控制权让出来,工人去干另一个任务;等 IO 好了再回来。切换只发生在内存里,比线程切换便宜得多。
Note粗略对比:创建一个 async 任务约 0.3 微秒,切换约 0.2 微秒;而创建一个线程约 17 微秒,切换约 1.7 微秒。IO 密集场景下,异步的省电是实打实的。
1-2 Future 是惰性的
Rust 里异步的核心是 Future(未来)。你可以把它理解成”一个将来某个时刻才会真正跑完的任务计划”。
关键点:Future 是惰性的。光创建一个 Future,它不会自己跑。必须有人去”轮询”(poll)它,它才开始执行;遇到阻塞(比如等 IO),它就让出控制权,等下次被唤醒再继续,直到完成。这点跟 JavaScript 的 Promise 不同——Rust 的 Future 不主动执行,没人 poll 就一直躺着。
用 async fn 或 async { } 就能造出一个返回 Future 的东西:
async fn foo() -> u8 {
5
}
fn bar() -> impl Future<Output = u8> {
async {
let x: u8 = foo().await;
x + 5
}
}
foo() 返回的是 Future<Output = u8>,意思是这个 Future 跑完后会给你一个 u8。async 语句块同理。注意:Output 就是 Future 完成时产出的类型。
1-3 .await 让出线程,不是阻塞线程
.await 是驱动 Future 前进的方式,也是异步和同步写法最像的地方。但有个本质区别要刻进脑子:
.await 遇到阻塞时,是”让出当前线程的控制权”,而不是”阻塞当前线程”。
什么意思?假设线程上有两个 Future 在跑。Future A 执行到 .await 卡在等 IO,它不会把整个线程堵死,而是把线程让出来;于是线程转头去执行 Future B。等 A 等的 IO 好了,A 再被唤醒,拿回线程接着跑。一个线程就这样在多个任务之间来回切,实现了并发。
对比一下:block_on(future) 是”硬等”——它会真的阻塞当前线程,直到 Future 跑完。所以 .await 是异步里最常用的推进方式,而 block_on 更像是一个把异步结果”同步地”逼出来的入口(运行时的执行器常用它)。
use futures::executor::block_on;
async fn hello() {
println!("你好,世界!");
}
fn main() {
let future = hello(); // 只创建 Future,不打印任何东西
block_on(future); // 真正跑起来,才打印
}
如果只写 hello(); 而不 block_on 或 .await,编译器会警告你:Future 创建了却没被使用,什么都不会发生。
1-4 用 .await 串起多个异步调用
在 async fn 里调用另一个异步函数,记得加 .await,否则那个 Future 永远不会执行:
use futures::executor::block_on;
async fn learn_song() -> String {
String::from("《菊花台》")
}
async fn sing_song(song: String) {
println!("献上一首 {}", song);
}
async fn dance() {
println!("身体动了起来");
}
async fn learn_and_sing() {
let song = learn_song().await; // 等学歌完成
sing_song(song).await; // 再等唱歌完成
}
fn main() {
block_on(async {
let f1 = learn_and_sing();
let f2 = dance();
futures::join!(f1, f2); // 两个 Future 并发跑
});
}
这里”学歌→唱歌”有先后顺序,但这两件事都能和”跳舞”同时进行。futures::join! 让 learn_and_sing 和 dance 并发:谁卡在 .await,就把线程让给另一个。用同步的 block_on 一个一个等,反而失去了并发的意义。join! 来自 futures 这个官方维护的包(在 Cargo.toml 里加 futures = "0.3")。
1-5 async 与多线程怎么选
两者都能并发,但不能互通,切换要重写代码,所以一开始就要选对:
- 有大量 IO 任务要并发(web 服务、数据库连接):选 async。
- 有部分 IO 任务、想省线程开销:可用多线程 + 线程池。
- 大量 CPU 密集任务要并行(并行计算):选多线程,线程数约等于 CPU 核心数。
- 无所谓、图简单:统一选多线程,代码直观得多。
async 也有代价:编译器会为每个 async 函数生成状态机,打包进运行时,导致编译出的二进制体积明显变大;而且 async 里生命周期、Pin 这些进阶概念更容易冒出来,学习曲线比线程陡。所以别盲目追异步,“简单够用”往往更重要。
1-6 2024 Edition 的兼容性说明
我们全书以 Rust 1.97.1 / 2024 Edition 为基线。对 async 而言,好消息是:基础的 async/await/Future 用法在 2024 Edition 下完全一致,没有破坏性改动,你前面学的写法照常工作。
2024 Edition 给 async 带来的主要便利是(来源:Rust 官方博客 2025-02-20 宣布 Rust 1.85.0 暨 2024 Edition 稳定):
Future和IntoFuture被加进了 prelude。这意味着你不再需要到处写use std::future::Future;,直接用Future这个名字即可,更接近其它语言的心智。AsyncFn、AsyncFnMut、AsyncFnOnce等异步 trait 得到稳定,异步闭包(async closure)也被稳定,写”像函数一样的异步块”更顺手。- 闭包对
Future的捕获更精细了:2021 Edition 里有时要被迫写async move {},2024 Edition 下很多情况不再需要。
Tip如果你看到老教程里第一行就有
use std::future::Future;,别奇怪——那是 2021 Edition 或更早的写法。在 2024 Edition 项目里,这行可以省掉。但写上也不会报错,兼容性好。
需要提醒一点:async 运行时(如 tokio)和 async 代码本身有部分仍在演进,不同运行时(tokio 与 async-std)之间不完全互通。不过日常用成熟运行时,足够稳定。下一章我们就用社区最主流的 tokio 把异步跑起来。
1-7 Future 的底层一瞥
好奇的话,可以看一眼 Future 这个 trait 长什么样:
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
poll 就是”推进一下”的意思,执行器反复调用它;返回 Poll::Pending 表示还没完、让出控制权,返回 Poll::Ready(val) 表示完成、给出结果。编译器把你写的 async fn 自动展开成实现了这个 trait 的状态机——每次 .await 都是状态机里的一个暂停点。你在第 74 章之后学到的很多”魔法”,底层都依赖于此。
日常写异步不必手搓 poll,tokio 这样的运行时替你调度。但知道它存在,遇到”为什么我的 Future 不执行""为什么状态机这么大”这类高级问题时,你就不会慌。理解底层,是为了在必要时能往下钻,而不是每天钻。
1-8 小结
异步的核心是 Future 惰性 + .await 让出线程。它用极少资源支撑海量并发任务,特别适合 IO 密集场景;CPU 密集任务还是多线程更合适。把 Future 纳入 prelude 确实是 2024 Edition 的改动;但「异步 trait(async fn in trait)」早在 Rust 1.75 就已稳定,并非 2024 Edition 的功劳——两者不是一回事。对你来说,基础用法都不变。光有 Future 还跑不起来,需要一个执行器去 poll 它——下章 tokio 登场。