Rust 异步编程:async/.await 初学者指南

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 不会执行任何操作。"

有两种解决方案:

  1. 使用 .await 语法。
  2. 手动轮询 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 不包含内置的异步运行时;第三方库如 tokioasync-stdsmol 提供了此功能。

原文:dev.to/leapcell/as...

相关推荐
明月看潮生4 小时前
青少年编程与数学 02-019 Rust 编程基础 13课题、智能指针
开发语言·青少年编程·rust·编程与数学
Python私教14 小时前
征服Rust:从零到独立开发的实战进阶
服务器·开发语言·rust
Python私教19 小时前
Rust:重新定义系统编程的安全与效率边界
开发语言·安全·rust
明月看潮生1 天前
青少年编程与数学 02-019 Rust 编程基础 12课题、所有权系统
开发语言·青少年编程·rust·编程与数学
景天科技苑1 天前
【Rust trait特质】如何在Rust中使用trait特质,全面解析与应用实战
开发语言·后端·rust·trait·rust trait·rust特质
heroboyluck2 天前
rust 全栈应用框架dioxus server
rust·全栈·dioxus
蜗牛沐雨2 天前
Rust 中的 `PartialEq` 和 `Eq`:深入解析与应用
开发语言·后端·rust
Python私教2 天前
Rust快速入门:从零到实战指南
开发语言·后端·rust
明月看潮生2 天前
青少年编程与数学 02-019 Rust 编程基础 10课题、函数、闭包和迭代器
开发语言·青少年编程·rust·编程与数学
明月看潮生2 天前
青少年编程与数学 02-019 Rust 编程基础 09课题、流程控制
开发语言·算法·青少年编程·rust·编程与数学