async/await 基础
问题
Rust 的 async/await 是如何工作的?与其他语言的异步模型有什么区别?
答案
基础语法
// async fn 返回一个 Future
async fn fetch_data(url: &str) -> String {
// .await 暂停当前任务,让出线程
let response = reqwest::get(url).await.unwrap();
response.text().await.unwrap()
}
// 等价于返回 impl Future
fn fetch_data(url: &str) -> impl Future<Output = String> + '_ {
async move {
let response = reqwest::get(url).await.unwrap();
response.text().await.unwrap()
}
}
核心概念:Future 是惰性的
与 JavaScript 的 Promise 不同,Rust 的 Future 不会在创建时立即执行:
async fn hello() {
println!("Hello!");
}
fn main() {
let future = hello(); // 什么都没发生!
// 必须交给执行器(runtime)驱动
tokio::runtime::Runtime::new().unwrap().block_on(future);
// 现在才打印 "Hello!"
}
async 块
async fn process() {
// async 块
let result = async {
let a = fetch_a().await;
let b = fetch_b().await;
a + b
}.await;
// 同时启动多个任务
let (a, b) = tokio::join!(fetch_a(), fetch_b());
// 等待第一个完成
tokio::select! {
val = fetch_a() => println!("A 先完成: {}", val),
val = fetch_b() => println!("B 先完成: {}", val),
}
}
async 与线程的对比
| 特性 | OS 线程 | async 任务 |
|---|---|---|
| 内存占用 | ~8MB 栈 | ~几百字节 |
| 上下文切换 | 系统调用 | 用户态 |
| 创建开销 | 重 | 轻 |
| 适用并发数 | 数千 | 数十万 |
| CPU 密集型 | ✅ | ❌(会阻塞运行时) |
async 的编译产物
编译器将 async fn 转换为状态机:
async fn example() {
let a = step_1().await; // 状态 0 → 状态 1
let b = step_2(a).await; // 状态 1 → 状态 2
println!("{}", b);
}
// 编译器生成(伪代码):
enum ExampleFuture {
State0 { /* step_1 的 future */ },
State1 { a: i32, /* step_2 的 future */ },
State2, // 完成
}
常见面试问题
Q1: Rust 的 async 和 JavaScript 的 async 有什么区别?
答案:
| 特性 | Rust | JavaScript |
|---|---|---|
| Future/Promise | 惰性(需要 poll) | 立即执行 |
| 运行时 | 不内置,需选择(Tokio) | 内置事件循环 |
| 底层实现 | 编译为状态机 | Promise 链 |
| 多线程 | 支持多线程运行时 | 单线程 |
| 性能 | 零成本抽象 | 有分配开销 |
Q2: 为什么不能在 async 中使用标准库的 Mutex?
答案:
标准库 Mutex::lock() 会阻塞当前线程。在 async 运行时中,一个线程可能跑多个任务。如果一个任务持有锁并 .await(让出线程),其他任务想获取同一个锁就会死锁。
解决方案:
- 锁持有时间短且不跨
.await→ 标准库 Mutex 没问题 - 锁持有跨
.await→ 使用tokio::sync::Mutex
Q3: tokio::join! 和 tokio::spawn 有什么区别?
答案:
join!:在当前任务内并发运行多个 Future,不创建新任务spawn:创建独立任务,由运行时调度到任何线程
// join!:同一任务内并发(结构化并发)
let (a, b) = tokio::join!(future_a, future_b);
// spawn:独立任务(非结构化并发)
let handle = tokio::spawn(async { /* ... */ });
let result = handle.await?;
Q4: async fn 返回的 Future 是 Send 的吗?
答案:
取决于 .await 点跨越的数据是否都是 Send。如果跨 .await 持有的变量包含 Rc、MutexGuard 等非 Send 类型,Future 就不是 Send,无法在多线程运行时上运行。