Rust 的 async/await 简介,涵盖 Futures、执行器(executors)和并发,并附有实用示例。
Rust 中的异步编程:async/.await
async/.await 是 Rust 语言内置的一项特性,它允许我们以同步风格编写异步代码。
让我们通过示例学习如何使用 async/.await 关键字。在开始之前,我们需要引入 futures 包。请编辑 Cargo.toml 文件并添加以下内容:
[dependencies]
futures = "0.3"
使用 async 创建异步 Future
简单来说,async 关键字可用于创建以下类型的 Future:
- 定义一个函数:
async fn - 定义一个代码块:
async {}
例如,一个异步函数:
async fn hello_world() {
...
}
async 关键字会修改函数的返回类型,使其返回一个实现了 Future trait 的对象。然后它将执行结果包装在一个新的 Future 中并返回,大致等价于:
fn hello_world() -> impl Future<Output = ()> {
async { ... }
}
注意:async 代码块实现了一个匿名的 Future trait 对象,其内部封装了一个生成器(Generator)。这个生成器本身是一个实现了 Future 的状态机。当 async 块内的任何操作返回 Poll::Pending 时,生成器会调用 yield,交出当前线程的控制权。一旦被恢复执行,生成器将继续运行,直到所有代码执行完毕,即状态机进入“完成”(Complete)状态,并返回 Poll::Ready,表示该 Future 已执行完成。
标记为 async 的代码块会被转换为一个实现了 Future trait 的状态机。与同步调用会阻塞当前线程不同,当一个 Future 遇到阻塞操作时,它会主动释放当前线程的控制权,等待其他 Future 的执行结果。
一个 Future 需要在执行器(executor)上运行。例如,block_on 就是一个会阻塞当前线程的执行器:
// block_on 会阻塞当前线程,直到指定的 Future 执行完成。
// 这种方式简单直接,但其他运行时执行器提供了更复杂的行为,
// 例如使用 join 在同一线程上调度多个 futures。
use futures::executor::block_on;
async fn hello_world() {
println!("hello, world!");
}
fn main() {
let future = hello_world(); // 返回一个 Future,因此此时不会打印任何内容
block_on(future); // 执行该 Future 并等待其完成;此时才会打印 "hello, world!"
}
使用 .await 等待另一个异步 Future 完成
在上面的 main 函数中,我们使用了 block_on 执行器来等待 Future 完成,使代码看起来是同步的。但如果需要在一个 async fn 内部调用另一个 async fn,并等待其完成后才执行后续代码,该怎么办?例如:
use futures::executor::block_on;
async fn hello_world() {
// 在 async 函数内部直接调用另一个 async 函数——这样可行吗?
hello_cat();
println!("hello, world!");
}
async fn hello_cat() {
println!("hello, kitty!");
}
fn main() {
let future = hello_world();
block_on(future);
}
在这里,hello_world 异步函数中首先调用了另一个异步函数 hello_cat,然后打印 "hello, world!"。让我们看看输出结果:
warning: unused implementer of `futures::Future` that must be used
--> src/main.rs:6:5
|
6 | hello_cat();
| ^^^^^^^^^^^^
= note: futures do nothing unless you `.await` or poll them
...
hello, world!
正如预期,我们在 main 中使用 block_on 执行了 Future,但 hello_cat 返回的 Future 从未被执行。幸运的是,编译器给出了友好的警告:“Futures do nothing unless you .await or poll them.”(除非你对 Future 调用 .await 或手动轮询,否则它什么也不会做。)
有两种解决方案:
- 使用
.await语法。 - 手动轮询 Future(这种方式更复杂,本文不展开)。
让我们使用 .await 修改代码:
use futures::executor::block_on;
async fn hello_world() {
hello_cat().await;
println!("hello, world!");
}
async fn hello_cat() {
println!("hello, kitty!");
}
fn main() {
let future = hello_world();
block_on(future);
}
在 hello_cat() 后添加 .await 后,输出发生了显著变化:
hello, kitty!
hello, world!
现在输出顺序严格遵循代码顺序。这意味着我们在保持顺序编码风格的同时,实现了异步执行。这种方式简单、高效,并消除了“回调地狱”(callback hell)。
在内部,每个 .await 的行为类似于一个执行器,它会反复轮询 Future 的状态。如果返回 Pending,就调用 yield;否则退出循环并完成 Future 的执行。其逻辑大致如下:
loop {
match some_future.poll() {
Pending => yield,
Ready(x) => break
}
}
简而言之,在 async fn 内部使用 .await 可以等待另一个异步调用完成。然而,与 block_on 不同,.await 不会阻塞当前线程。相反,它会异步等待 Future A 完成。在等待期间,线程可以继续执行其他 Future B 实例,从而实现并发。
一个示例
考虑一个唱歌和跳舞的场景。如果不使用 .await,实现可能如下所示:
use futures::executor::block_on;
struct Song {
author: String,
name: String,
}
async fn learn_song() -> Song {
Song {
author: "Rick Astley".to_string(),
name: String::from("Never Gonna Give You Up"),
}
}
async fn sing_song(song: Song) {
println!(
"Performing {}'s {} ~ {}",
song.author, song.name, "Never gonna let you down"
);
}
async fn dance() {
println!("Dancing along to the song");
}
fn main() {
let song = block_on(learn_song()); // 第一次阻塞调用
block_on(sing_song(song)); // 第二次阻塞调用
block_on(dance()); // 第三次阻塞调用
}
这段代码能正确运行,但需要三次连续的阻塞调用,一次只完成一个任务。实际上,我们可以一边唱歌一边跳舞:
use futures::executor::block_on;
struct Song {
author: String,
name: String,
}
async fn learn_song() -> Song {
Song {
author: "Rick Astley".to_string(),
name: String::from("Never Gonna Give You Up"),
}
}
async fn sing_song(song: Song) {
println!(
"Performing {}'s {} ~ {}",
song.author, song.name, "Never gonna let you down"
);
}
async fn dance() {
println!("Dancing along to the song");
}
async fn learn_and_sing() {
let song = learn_song().await;
sing_song(song).await;
}
async fn async_main() {
let f1 = learn_and_sing();
let f2 = dance();
// join! 宏可以并发地运行多个 futures
futures::join!(f1, f2);
}
fn main() {
block_on(async_main());
}
在这里,学习歌曲和唱歌有严格的先后顺序,但这两者可以与跳舞同时进行。如果不使用 .await,而是用 block_on(learn_song()),就会阻塞当前线程,从而无法执行包括跳舞在内的任何其他任务。
因此,.await 在 Rust 的异步编程中至关重要。它允许多个任务在同一线程上并发运行,而不是顺序执行。
总结
async/.await 是 Rust 内置的用于编写异步函数的工具,这些函数看起来像同步代码。async 将代码块转换为一个实现了 Future trait 的状态机,而该状态机必须在执行器上运行。与阻塞整个线程不同,Future 会主动让出控制权,从而允许其他 Future 执行。
核心要点:
- Future 表示一个在未来会产生值的任务。
async用于创建一个 Future。.await用于轮询一个 Future,等待其完成。- 执行器(Executors)(如
block_on)负责管理和执行 Futures。 - Rust 的 async 是零成本抽象:没有堆分配,也没有动态分发。
- Rust 不包含内置的异步运行时;第三方库如
tokio、async-std和smol提供了这一功能。
总之,async/.await 使得 Rust 能够高效、并发地执行任务,消除回调地狱,并让异步编程变得直观易懂。