Appearance
25.5 异步编程
async 和 await 关键字是目前许多语言都采用的主流方案,使用关键字而不是用宏来做 API,有助于社区的统一性,避免不同的异步方案使用完全不一样的用户 API。
在 Rust 语言和标准库中,只有极少数必须的关键字、trait 和类型,这也是 Rust 一贯的设计思路。但凡是可以在第三方库中实现的,一律在第三方库中实现,哪怕这个库本来就是官方核心组维护的,这样做可以让这个库的版本升级更灵活,有助于标准库的稳定性。
从上一章节我们知道,Rust 的异步编程模型基于 Future 机制(底层实现是通过 Coroutine 状态机),而 async/await 语法糖让异步代码看起来像是同步的。
25.5.1 核心构件 Future
Future 是一个 Trait,Future 可以组合,一个 Future 可以由其他的一个或者多个 Future 包装而成。跟我们已经见过的迭代器 Iterator 很像。
其简化定义如下:
rust
// rust-lib/core/src/future/future.rs
use crate::ops;
use crate::pin::Pin;
use crate::task::{Context, Poll};
pub trait Future {
// 这就是“凭证”未来能兑换的“商品”类型
type Output;
// “轮询”一下,看看“商品”到货了没
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}这里有两个关键点:
Output类型:定义了这个Future完成后会返回什么类型的值。比如,一个网络请求的Future,其Output可能是一个String或Vec<u8>。poll方法:这是Future的心脏。它的作用就是去“问一下”:“我的任务完成了吗?”poll方法的返回值Poll<T>是一个枚举:rustpub enum Poll<T> { Ready(T), Pending, }想象一下,你不断地去问餐厅前台:“我的外卖好了吗?”
你 -> poll() -> 前台 | +--> "还没好呢!" (返回 Poll::Pending) | +--> "好了,给你!" (返回 Poll::Ready(你的外卖))
但是我们不断使用 poll 来查询状态,效率太低了。你告诉前台:“我的外卖好了,请打电话通知我。” 这个“电话号码”就是 Waker。
poll 方法的参数 cx: &mut Context<'_> 就包含了这个 Waker。
- 当一个
Future的poll方法被调用,发现任务还没准备好(例如,网络数据还没到),它会返回Poll::Pending。 - 在返回之前,它会“克隆”一份
Waker并存起来。 - 当底层事件完成时(例如,操作系统通知网络数据已到达),它会调用
Waker的wake()方法。 - 这个
wake()调用就像是给执行器(Executor,下一节讲)打了个电话:“嘿,那个任务现在有进展了,快再来poll它一次吧!”
我们有了可以被暂停和唤醒的任务(Future),但谁来管理和驱动它们呢?答案是 Executor(调度器/执行器),也常被称为 Runtime(运行时)。它的具体实现可以由第三方库来实现。Executor 就像是厨房中统筹全局的厨师长。他手里有一份“任务清单”(一堆 Futures),他会不断地轮询这些任务,推动它们向前执行。
一个 Executor 的基本工作流程:
- 接收任务:你将一个顶层的
Future交给Executor。 - 事件循环:
Executor启动一个循环,从任务队列中取出一个任务。 - 驱动任务:调用任务的
poll方法。 - 处理结果:
Poll::Ready(value):任务完成!Executor可以将结果交给用户,或者处理下一个任务。Poll::Pending:任务被暂停。Executor会把它放到一边,这样就不会占用 CPU,等待着被Waker唤醒。当Waker被调用时,Executor会把这个任务重新放回待执行的队列里。
- 重复:
Executor不断重复这个过程,确保所有任务都能在适当的时候得到执行。
Rust 标准库只提供了 Future trait 等核心构建块,但 没有内置 Executor。这是 Rust 的设计哲学:保持标准库的精简和稳定。
你需要选择一个第三方的
Runtime库来实际运行你的异步代码。最流行的选择是:
tokio: 社区生态最庞大,功能全面,为生产环境设计。async-std: API 设计上力求与标准库同步std模块相似,学习曲线平缓。
大家可以看到,Future 跟 Coroutine 一样,具备同样的特性,也就是说可以在某个地方主动中断执行,待下一次再进来的时候,刚好可以从上次退出的地方恢复执行。这就是为什么 Rust 的 Future 最终是基于 Coroutine 实现的。 在 Rust 里面,Coroutine 是 Future 的基础设施。
一般情况下,实现任务调度以及为通过各种异步操作实现 Future trait 并不是最终用户关注的问题,这些应该都已经被网络开发框架完成,比如 tokio。大部分用户需要关注的是如何利用这些框架完成业务逻辑。
25.5.2 语法糖 async/await
手动实现 Future 和管理 poll 逻辑非常繁琐。所以有了 async/await 语法,可以让编译器为我们处理所有复杂的工作。
调用一个 async 函数 仅仅是创建了一个 Future 实例,它什么都还没做!就像你拿到了“取餐凭证”,但饭还没开始做。你必须把这个返回的 Future 交给一个 Executor 来运行,它才会真正被执行。
早期的 await!
在 Rust 的异步编程早期(2018-2019 年左右),Rust 还没有正式支持 async/await 语法。当时,异步编程主要通过 futures 库实现。为了方便开发者在异步代码中等待某个 Future 的结果,futures 库提供了一个宏 await!。这个宏的作用是将异步的 Future 转换为阻塞式的结果,类似于后来正式引入的 .await 关键字。
早期,你可能会看到这样的代码:
rust
use futures::Future;
fn some_async_function() -> impl Future<Output = i32> {
async { 42 }
}
fn main() {
let result = futures::executor::block_on(async {
let value = await!(some_async_function());
value
});
println!("{}", result);
}对于 await 这个宏,我们可以在标准库中看到它的实现:
rust
macro_rules! await {
($e:expr) => { {
let mut pinned = $e;
let mut pinned = unsafe { $crate::mem::PinMut::new_unchecked(&mut pinned) };
loop {
match $crate::future::poll_in_task_cx(&mut pinned) {
$crate::task::Poll::Pending => yield,
$crate::task::Poll::Ready(x) => break x,
}
}
} }
}如果你在非常老旧的代码库中工作,可能仍然会遇到 await! 宏。这种情况下,你需要确保项目依赖了旧版的 futures 库(例如 futures 0.1)。但我强烈建议将代码升级到现代 Rust,使用 .await 语法,因为 await! 已经不再维护,且旧版库可能存在兼容性问题。
现代的 async/await
自 Rust 1.39.0 开始,Rust 正式引入了 async/await 语法,await! 宏被废弃,取而代之的是 .await 关键字。从语法上讲,.await 只能出现在 async 函数、async 闭包或 async 代码块里,不然编译器直接报错。
编译器会将一个 async 函数转换成一个状态机(State Machine)。
rust
async fn fetch_and_process() {
// --- 状态 0 ---
let url = "https://www.rust-lang.org";
let response = fetch(url).await; // 暂停点 1
// --- 状态 1 ---
let text = response.text().await; // 暂停点 2
// --- 状态 2 ---
println!("网页内容: {}", text);
// --- 最终状态 ---
}这个状态机实现了 Future trait。每次 Executor 来 poll 它:
- 如果处于状态 0,它会执行
fetch(url),然后poll返回的Future。 - 如果返回
Poll::Pending,那么整个fetch_and_process函数就会暂停,整个状态机就 暂停(yield) 在状态 0,并把控制权交还给Executor,让它可以去执行其他任务。 - 当
fetch(url)的Future被Waker唤醒(resume) 并最终返回Poll::Ready(value)时,Executor会回到fetch_and_process函数暂停的地方,把value赋值给response,然后继续向下,会进入状态 1。
- 如果处于状态 1,它会执行
response.text(),然后poll返回的Future,逻辑同上。 - ...以此类推,直到最终状态。
可以看到,使用 async/await 来写异步逻辑,一方面可以保证高效率,另一方面,代码流程还是跟普通的同步逻辑类似,比较符合直觉。
每次碰到 .await,就是告诉调度器:“我等一会儿,别让我的代码占用 CPU,其他任务可以先执行;东西准备好了再叫我回来。”。这一块被编译器包装进生成的状态机,巧妙地隐藏了所有 poll, Pending, Ready 和 Waker 的细节,自动帮你管理状态、挂起、唤醒流程。
下面给大家解释一下 async 和 await 分别做了什么事情。
async 关键字可以修饰函数、闭包以及代码块。对于函数:
rust
async fn read_from_db(id: u32) -> String {
// ... 模拟数据库查询 ...
"some data".to_string()
}在编译器看来,等价于这个返回 impl Future 的普通函数::
rust
fn read_from_db_desugared(id: u32) -> impl Future<Output = String> {
// 编译器在这里会为你生成一个实现了 Future 的匿名状态机
// ...
}这两种写法实际上是一模一样的。凡是被 async 修饰的函数,返回的都是一个实现了 Future trait 的类型。 由 async 修饰的闭包也是一样的。async 代码块同样类似。它相当于创建了一个语句块表达式,这个表达式的返回类型是 impl Future。
async 关键字不仅对函数签名做了一个改变,而且对函数体也自动做了一个包装,被 async 关键字包起来的部分,会自动产生一个 Coroutine,并把这个 Coroutine 包装成一个满足 Future 约束的结构体。在函数体中用户需要返回的是 Future::Output 类型,也就是异步任务成功后得到的结果,比如 String、io::Result<T> 等等。
25.5.3 总结
让我们回顾一下 Rust 异步编程的全景图:
- 为什么异步? 为了高效处理大量并发 I/O 任务,避免线程阻塞,就像一个聪明的厨师。
Future是什么? 是一个异步任务的“凭证”,代表一个未来会完成的值。它可以通过poll方法查询状态(Ready或Pending)。Executor是什么? 是任务的“调度器”或“运行时”(如tokio),负责驱动Futures 执行,并在它们Pending时暂停,在被Waker唤醒后继续。async/await是什么? 是 Rust 提供的“语法糖”,让我们能用近乎同步的方式编写异步代码。async将函数变为Future生成器,.await则优雅地处理了任务的暂停和恢复。
通过这套精心设计的系统,Rust 成功地在提供极致性能的同时,保持了其赖以成名的内存安全保证,让编写大规模、高并发的程序变得前所未有的简单和安全。