Rust 中的 async/await 协程

FreeGuideOnline 最新 2026-07-11

Rust 中的 async/await 协程

Rust 的 async/await 语法为编写异步代码提供了零成本抽象的便利。它建立在 Future trait 之上,通过协作式调度实现高并发。本教程将带你从零开始,理解 Rust 异步编程的核心概念,并使用 tokio 运行时编写可运行的异步程序。

为什么要用 async?

传统多线程在处理大量 I/O 密集型任务时,线程切换开销大且内存占用高。异步编程在一个或少量线程上通过任务调度,可以让上千个任务同时“等待”而不阻塞,极大提高 I/O 密集型应用的吞吐量。

Rust 的 async 将这种能力与内存安全和零成本抽象结合:异步代码会被编译为状态机,没有运行时开销(除运行时库外)。

核心概念:Future

Future 是一个标准库中的 trait,代表一个尚未求值的异步计算。

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),
    Pending,
}
  • poll 方法被调用时,如果任务已经完成,返回 Poll::Ready(output);如果尚未完成,则注册唤醒器并返回 Poll::Pending
  • 你不必手动实现 Future,而是使用 async 块或 async fn 来生成实现了 Future 的匿名类型。

async 块和 async 函数

async 块创建一个 Future,它可以包含 .await 点。

let future = async {
    // 一些异步操作
    42
};

async fn 语法糖将函数返回类型转为 impl Future<Output = ...>

async fn fetch_data() -> String {
    // 模拟异步操作
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    "data".to_string()
}

只有对 Future 执行 .await,它才会被驱动执行。.await 只能在 async 块或 async fn 内部使用。

运行时与执行器

Future 本身是惰性的,需要一个运行时来轮询它们。常用的异步运行时是 tokio(单线程或多线程)。

在项目中添加依赖 (Cargo.toml):

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

主函数通常用 #[tokio::main] 标记,它会创建一个 Tokio 运行时并执行异步主函数。

#[tokio::main]
async fn main() {
    let result = fetch_data().await;
    println!("{}", result);
}

并发执行:join! 与 select!

多个异步任务可以同时运行,而不是顺序等待。

tokio::join!:同时运行多个 Future,等待所有完成。

use tokio::time::{sleep, Duration};

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

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

#[tokio::main]
async fn main() {
    let (r1, r2) = tokio::join!(task1(), task2());
    println!("r1: {r1}, r2: {r2}"); // 总耗时约2秒,而非3秒
}

tokio::select!:等待多个 Future,只要其中一个完成就返回,同时取消其余任务。

use tokio::time::{sleep, Duration};

async fn slow_task() -> &'static str {
    sleep(Duration::from_secs(5)).await;
    "slow"
}

#[tokio::main]
async fn main() {
    tokio::select! {
        _ = slow_task() => println!("slow task finished"),
        _ = sleep(Duration::from_secs(2)) => println!("timeout!"),
    }
}

生成独立任务:tokio::spawn

tokio::spawn 将一个 Future 提交给运行时,在后台独立运行,并返回一个 JoinHandle

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // 在独立的“协程”中运行
        "hello from spawned task"
    });

    let result = handle.await.unwrap();
    println!("{}", result);
}

注意:使用 spawn 时,要求 FutureSend + 'static,因为它需要在线程间传递。

错误处理

异步函数返回 Result 时,? 操作符可以像同步代码一样使用。

async fn fallible_work() -> Result<u32, Box<dyn std::error::Error>> {
    // 某些可能失败的操作
    Ok(42)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let value = fallible_work().await?;
    println!("{value}");
    Ok(())
}

Pin 与自引用结构

async 块编译后可能包含自引用(例如局部变量引用自身的字段)。为了保证内存安全,Future 被移动后指针可能失效,因此需要 Pin

大多数情况下你不需要直接处理 Pin,但需要注意:

  • async fn 返回的 Futuretokio::spawn 调用时,默认就是 Unpin 的(除非包含自引用)。
  • 如果你手动实现 Future,可能需要理解 Pinpoll 的用法。

简言之:在应用中你几乎不需要手动 pin!,但当你看到生命周期或 Pin 相关错误时,通常意味着你试图在异步闭包中持有对局部变量的引用,此时需要把数据移到堆上或以 Arc 共享。

流:异步迭代器

Stream trait(可在 futures crate 中找到)是异步版本的 Iterator,通常通过 while let Some(item) = stream.next().await 消费。

use futures::stream::{self, StreamExt};

#[tokio::main]
async fn main() {
    let stream = stream::iter(vec![1, 2, 3]);
    tokio::pin!(stream);

    while let Some(value) = stream.next().await {
        println!("{value}");
    }
}

需要添加依赖:futures = "0.3"

异步 trait 方法

标准库中的 trait 方法不能直接标记为 async fn,需要借助第三方 crate,如 async-trait

use async_trait::async_trait;

#[async_trait]
pub trait DataFetcher {
    async fn fetch(&self) -> String;
}

struct MyFetcher;

#[async_trait]
impl DataFetcher for MyFetcher {
    async fn fetch(&self) -> String {
        // 异步操作
        "data".into()
    }
}

这会将返回类型转换为 Pin<Box<dyn Future<Output = ...> + Send>>,注意带来的堆分配开销和 Send 约束。

常见模式与最佳实践

  • 避免在异步函数中执行密集计算:长时间的 CPU 工作会阻塞运行时线程,应使用 tokio::task::spawn_blocking 将其转移到专用线程池。
  • 善用 tokio::sync 模块:提供异步版本的 Mutex, RwLock, mpsc 等。
  • 测试异步代码tokio::test 宏可创建异步测试环境。
  • 使用 tracing 或日志:跟踪异步任务的生命周期。

总结

Rust 的 async/await 提供了高效且安全的异步抽象:

  • Future trait 定义可恢复的计算。
  • async 块和函数创建 Futureawait 驱动执行。
  • 运行时(如 Tokio)负责调度任务。
  • join!, select!, spawn 提供并发组合能力。
  • Pin, Send 等概念保证内存与线程安全。

掌握这些基础,你就可以开始构建高性能的异步 Rust 应用了。下一步可以深入阅读 Asynchronous Programming in Rust 官方指南,了解更多高级主题。