async/.await
是 Rust 语言内置的功能,允许我们以同步的方式编写异步代码。
让我们通过示例学习如何使用 async/.await
。
首先,我们需要引入 futures
包。
Cargo.toml
添加以下内容:
toml
[dependencies]
futures = "0.3"
创建异步 Future
简单来说,async
可以创建以下类型的 Future
:
- 定义函数:
async fn
- 定义代码块:
async {}
例如,async
函数:
rust
async fn hello_world() {
// 函数体
}
async
关键字修改了函数,使其返回一个 Future
对象。它将执行结果包装在一个新的 Future
中,大致相当于:
rust
fn hello_world() -> impl Future<Output = ()> {
async { /* 函数体 */ }
}
注意: async
代码块实现了一个匿名的 Future
,封装了一个 Generator
,实现一个 Future
的生成器。Generator
实际上充当一个状态机。当 async
代码块中的任何操作返回 Poll::Pending
时,生成器调用 yield
。重新启动后,生成器执行直到所有代码完成,即状态机进入 Complete
状态并返回 Poll::Ready
,表示 Future
已完成执行。
async
的代码块被转换成一个状态机。与同步调用不同,当 Future
遇到阻塞操作时,它会放弃当前线程的控制权,等待其他 Future
的执行结果。
Future
需要在执行器上运行。例如,block_on
是一个阻塞当前线程的执行器:
rust
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
并在执行后续代码之前等待其完成,该怎么办?例如:
rust
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!"
。我们来看看输出:
rust
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
未被执行。编译器警告: "除非 .await
或轮询它们,否则 Futures 不会执行任何操作。"
有两种解决方案:
- 使用
.await
语法。 - 手动轮询
Future
。
让我们使用 .await
修改代码:
rust
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
后,输出发生了显著变化:
rust
hello, kitty!
hello, world!
现在输出顺序严格遵循代码顺序。这意味着我们以异步的方式执行了代码,同时保持了顺序编码的风格。这种方法简单、高效,并且避免了回调地狱。
内部来说,每个 .await
都像是一个执行器,反复轮询 Future
的状态。如果返回 Pending
,它会调用 yield
。否则,它退出循环并完成 Future
的执行。逻辑大致如下:
rust
loop {
match some_future.poll() {
Pending => yield,
Ready(x) => break
}
}
简而言之,在 async fn
中使用 .await
可以等待另一个异步调用完成。与 block_on
不同,.await
不会阻塞当前线程。相反,它异步等待 Future A
完成。在等待期间,线程可以继续执行其他 Future B
实例,从而实现并发。
示例
考虑一个唱歌和跳舞的场景。如果没有 .await
,实现可能如下所示:
rust
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()); // 第三个阻塞调用
}
这段代码虽然能够正确运行,但需要阻塞调用,一次完成一个任务。实际上,我们可以同时唱歌和跳舞:
rust
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!` 宏并发运行多个 Future
futures::join!(f1, f2);
}
fn main() {
block_on(async_main());
}
在这里,学习和唱歌有严格的顺序,但两者可以与跳舞同时进行。如果没有 .await
,使用 block_on(learn_song())
会阻塞当前线程,阻止任何其他任务。
因此,.await
在 Rust 的异步编程中至关重要。它允许在同一线程上并发运行多个任务,而不是按顺序执行。
总结
async/.await
是 Rust 编写同步的内置工具。async
将代码块转换为实现 Future
特征的状态机。与阻塞线程不同,Future
会放弃控制权,允许其他 Future
执行。
关键要点:
Future
表示一个将在未来产生值的任务。async
创建一个Future
。.await
轮询一个Future
,等待其完成。- 执行器(如
block_on
)管理和执行Future
。 - Rust 的
async
是零成本的:没有堆分配或动态分派。 - Rust 不包含内置的异步运行时;第三方库如
tokio
、async-std
和smol
提供了此功能。