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...

相关推荐
a cool fish(无名)14 分钟前
rust-方法语法
开发语言·后端·rust
a cool fish(无名)15 小时前
rust-参考与借用
java·前端·rust
叶 落18 小时前
[Rust 基础课程]猜数字游戏-获取用户输入并打印
rust·rust基础
RustFS21 小时前
RustFS 如何修改默认密码?
rust
景天科技苑1 天前
【Rust线程池】如何构建Rust线程池、Rayon线程池用法详细解析
开发语言·后端·rust·线程池·rayon·rust线程池·rayon线程池
该用户已不存在2 天前
Zig想要取代Go和Rust,它有资格吗
前端·后端·rust
用户1774125612442 天前
不懂装懂的AI,折了程序员的阳寿
rust
量子位3 天前
vivo自研蓝河操作系统内核开源!Rust开发新机遇来了
rust·ai编程
祈澈菇凉3 天前
rust嵌入式开发零基础入门教程(六)
stm32·单片机·rust
祈澈菇凉3 天前
rust嵌入式开发零基础入门教程(二)
开发语言·后端·rust