文章目录

Rust 异步编程:async/await 与 futures

发布于 2026-04-18 06:28:21 · 浏览 14 次 · 评论 0 条

Rust 异步编程:async/await 与 futures

在 Rust 中处理高并发 I/O 操作(如网络请求、文件读写)时,传统的同步阻塞模式会严重浪费 CPU 资源。Rust 提供的 async/await 语法和 Future 机制,允许我们在单线程中高效处理大量并发任务。以下指南将带你从零掌握 Rust 异步编程的核心概念与实战技巧。


理解异步编程的必要性

同步编程在等待 I/O 操作(如读取文件)时,操作系统会挂起当前线程,直到数据准备就绪。如果在同一个线程中依次读取两个文件,总耗时是两者之和。

使用异步模式时,线程在发起读取请求后不会阻塞,而是转而去处理其他任务。当 I/O 操作完成时,操作系统会通知线程恢复处理。并发读取两个文件,总耗时仅取决于最慢的那个操作。

通过数学公式对比:

  • 同步模式总耗时:$T_{total} = T_{file1} + T_{file2}$
  • 异步模式总耗时:$T_{total} = \max(T_{file1}, T_{file2})$

掌握 async/await 基础语法

async/await 是 Rust 的语法糖,用于简化异步代码的编写。

  1. 声明 异步函数。
    fn 前加上 async 关键字。这会让函数返回一个实现了 Future trait 的值,而不是直接返回结果。

    async fn hello() {
        println!("Hello, world!");
    }
  2. 执行 异步代码。
    异步函数不会自动运行,必须使用执行器来驱动。最简单的驱动方式是使用 block_on,它会阻塞当前线程直到 Future 完成。

    首先,添加依赖到 Cargo.toml(以 async-std 为例):

    [dependencies]
    async-std = "1.10"
    futures = "0.3"

    然后,在 main 函数中调用

    use async_std::task;
    
    fn main() {
        // block_on 会阻塞主线程,直到任务完成
        task::block_on(hello());
    }
  3. 使用 .await 等待结果。
    .await 只能在 async 函数或块内部使用。它不会阻塞线程,而是挂起当前任务,等待底层 Future 准备好。

    use async_std::task;
    use std::time::Duration;
    
    async fn sleep_example() {
        println!("开始睡眠");
        // 使用异步 sleep,让出线程控制权
        task::sleep(Duration::from_secs(2)).await;
        println!("醒来");
    }

深入 Future 与执行模型

理解 async/await 的底层运作原理,有助于编写更高效的代码。

1. Future Trait 的核心定义

Future 是 Rust 异步编程的基石。它是一个状态机,表示一个可能尚未完成的计算。

标准库中的核心定义如下:

use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}
  • Output:Future 完成时产生的值的类型。
  • poll:推动 Future 执行的方法。
  • Poll:枚举类型,包含 Ready(T)(完成)和 Pending(未完成)两个状态。

2. 状态机流转机制

编译器会将 async fn 函数体转换为一个状态机。每当遇到 .await,状态机就会保存当前状态,并在 poll 返回 Pending 时退出。

以下流程图描述了 Future 从开始到完成的内部流转:

stateDiagram-v2 [*] --> Start Start --> Polling: Executor 调用 poll Polling --> Processing: 正在执行逻辑 Processing --> Waiting: 遇到 .await 且资源未就绪 Waiting --> Polling: Waker 通知唤醒 Processing --> Ready: 任务完成 Ready --> [*]: 返回结果

3. 执行器 与 唤醒机制

Rust 语言本身不包含异步运行时。Future 是惰性的,只有被 poll 时才会运行。这需要依赖第三方库(如 async-stdtokiofutures)提供的执行器。

  • Executor:负责调度任务,循环调用 poll
  • Waker:一个句柄,用于在 I/O 事件就绪时通知执行器再次 poll 对应的 Future。

执行器与 Waker 的协作流程如下:

graph LR A[Executor] -->|poll| B[Future] B -->|Pending| C[挂起任务] C -->|等待事件| D[IO 操作] D -->|事件就绪| E[Waker] E -->|wake| A

使用 futures 库处理并发

futures 库提供了许多实用的工具宏和组合子,用于控制异步流程。

1. 并发执行多个任务

使用 futures::join! 宏可以同时运行多个 Future,并等待它们全部完成。这与 JavaScript 的 Promise.all 类似。

注意:不要像写同步代码那样依次 .await,那会变成串行执行。

use async_std::task;
use futures::join;
use std::time::Duration;

async fn task_one() -> u32 {
    task::sleep(Duration::from_secs(1)).await;
    1
}

async fn task_two() -> u32 {
    task::sleep(Duration::from_secs(2)).await;
    2
}

fn main() {
    task::block_on(async {
        // 错误做法:串行,总耗时 3 秒
        // let r1 = task_one().await;
        // let r2 = task_two().await;

        // 正确做法:并发,总耗时 2 秒
        let (r1, r2) = join!(task_one(), task_two());

        println!("结果: {}, {}", r1, r2);
    });
}

2. 处理动态数量的 Future

如果需要处理的 Future 数量不固定,可以使用 futures::stream

use async_std::task;
use futures::stream::{self, StreamExt}; // StreamExt 提供 next 方法

async fn process(num: u32) -> u32 {
    task::sleep(Duration::from_millis(100 * num)).await;
    num * 2
}

fn main() {
    task::block_on(async {
        let nums = vec![1, 2, 3];

        // 创建一个流,并使用 buffer_unordered 进行并发处理
        let mut stream = stream::iter(nums)
            .map(|n| process(n))
            .buffer_unordered(2); // 限制并发数为 2

        while let Some(result) = stream.next().await {
            println!("处理结果: {}", result);
        }
    });
}

避开生命周期陷阱

async 块会捕获周围的变量,这可能会导致生命周期问题。如果生成的 Future 超出了变量本身的生命周期(例如将 Future 发送到另一个线程),编译器会报错。

  1. 识别 问题代码。

    use std::future::Future;
    
    async fn borrow_x(x: &u8) -> u8 {
        *x
    }
    
    fn bad_example() -> impl Future<Output = u8> {
        let x = 5;
        borrow_x(&x) // 错误:x 会在函数结束时被释放,但 Future 还活着
    }
  2. 使用 async move 解决。
    通过将变量移动到 async 块内部,确保变量的所有权归 Future 所有,从而延长其生命周期。

    fn good_example() -> impl Future<Output = u8> {
        async {
            let x = 5;
            borrow_x(&x).await // x 现在在这个 async 块内,生命周期与 Future 一致
        }
    }

异步运行时选择

Rust 生态中有多个主流运行时,根据项目需求选择:

运行时 特点 适用场景
tokio 功能最全,生态最丰富,性能极高 Web 服务器、微服务、复杂的网络应用
async-std API 设计简单,与标准库风格一致 简单的异步工具、教学、对标准库兼容性要求高的场景
futures 提供基础 Trait 和工具,不提供完整运行时 作为依赖库,提供通用的异步抽象

若选择 tokio,需启用 full 特性:

[dependencies]
tokio = { version = "1", features = ["full"] }

并在 main 函数上使用宏:

#[tokio::main]
async fn main() {
    println!("Hello from tokio!");
}

常见错误处理

在异步编程中,? 运算符可以直接用于 Future,前提是该 Future 返回的是 Result

use async_std::fs;

async fn read_file() -> std::io::Result<String> {
    // ? 会自动将 Err 提前返回,将 Ok 解包
    let content = fs::read_to_string("Cargo.toml").await?;
    Ok(content)
}

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文