Rust 中的 async/await 协程
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时,要求Future是Send + '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返回的Future被tokio::spawn调用时,默认就是Unpin的(除非包含自引用)。 - 如果你手动实现
Future,可能需要理解Pin和poll的用法。
简言之:在应用中你几乎不需要手动 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 提供了高效且安全的异步抽象:
Futuretrait 定义可恢复的计算。async块和函数创建Future,await驱动执行。- 运行时(如 Tokio)负责调度任务。
join!,select!,spawn提供并发组合能力。Pin,Send等概念保证内存与线程安全。
掌握这些基础,你就可以开始构建高性能的异步 Rust 应用了。下一步可以深入阅读 Asynchronous Programming in Rust 官方指南,了解更多高级主题。