异步编程延申
async 和 await
简单来说:async 负责"包装",await 负责"推进".
我们可以从以下两个维度来拆解:
1.async 阶段:静态的代码转换(编译期)
编译器会将 async 代码块转换成一个实现了 Future trait 的状态机(State Machine).
- 当你定义一个
async fn时,它不会立即执行任何逻辑. - 它仅仅是返回一个保存了该函数执行状态(局部变量、执行进度等)的结构体.
这和
for循环转Iterator非常相似:for本身只是语法糖,核心是那个能不断产生值的迭代器对象.
2.await 阶段:动态的执行驱动(运行期)
这里是容易产生误解的地方.await 并不是简单的"执行完毕",它是"挂起"与"再唤醒"的控制点.
- 它不是阻塞:
.await不会像sleep那样锁死线程. - 它是状态机的边界点:在编译器生成的状态机里,每一个
.await都是一个 Yield Point(让出点).
推进过程如下:
运行到 .await
│
▼
调用内部 Future 的 poll()
│
├─── Pending(未就绪)──→ 交出控制权,返回 Executor 等待唤醒
│
└─── Ready(已就绪)───→ 获取结果,继续执行后续逻辑总结
async | await | |
|---|---|---|
| 阶段 | 编译期 | 运行期 |
| 作用 | 将逻辑打包为 Future 对象 | 驱动状态机向前推进 |
| 类比 | 定义迭代器结构 | 调用 .next() |
⚠️ 如果没有
.await,Future就永远只是一个静止的结构体,不会有任何代码被实际执行.
Pin 和 Unpin
为什么需要 Pin?
在异步状态机中,Future 保存了跨 .await 点的局部变量.问题来了:
async fn example() {
let s = String::from("hello");
let r = &s; // r 引用了同一个栈帧内的 s
some_async_op().await;
println!("{}", r); // .await 之后还要用 r
}编译器会把 s 和 r 都放进生成的状态机结构体里.这就产生了自引用结构体——结构体内部有一个字段指向另一个字段.
危险在于:如果这个结构体在内存中被移动(Move),指针 r 还指向旧地址,就变成了悬垂指针!
移动前0x1000 移动后0x2000
┌────────────┐ ┌────────────┐
│ s: "hello" │ →移动→ │ s: "hello" │
│ r: 0x1000 │ │ r: 0x1000 │ ← 指向旧地址!已失效!
└────────────┘ └────────────┘Pin<P> 的作用就是在编译层面保证:被 Pin 住的值不会再被移动.
而这个保证是通过Rust 类型系统和借用检查器的"禁令"来实现的.简单理解就是被 Pin 包裹的值不能再转移所有权(T) 和 生成新的引用(&mut T) 来移动它.
Unpin:我不怕移动
Unpin 是一个 auto trait,绝大多数类型默认都实现了它.
- 实现了
Unpin的类型:可以安全地移动,Pin对它们没有实际约束效果. - 没有实现
Unpin的类型(如async生成的Future):Pin会认真保护它们.
可以把
Unpin理解为一个类型在说:"随便移动我,我内部没有自引用,不怕!"
直观类比
| 概念 | 类比 |
|---|---|
Pin | 把一封信钉在公告板上,它不能再被挪走 |
Unpin | 普通便利贴,随便移动,没关系 |
| 自引用 | 信里写着"见本信第3行",挪走后"第3行"就找不到了 |
简单示例
use std::pin::Pin;
// 一个普通的 Unpin 类型,Pin 对它没有实质限制
let mut x = 42i32;
let mut pinned = Pin::new(&mut x);
*pinned = 100; // 完全正常
// async fn 生成的 Future 是 !Unpin 的
// 需要用 Box::pin 或 pin! 宏将其固定在堆/栈上
let fut = async { println!("hello") };
let pinned_fut = Box::pin(fut); // 固定在堆上,可以安全 poll在实际调用 poll() 时,执行器要求传入 Pin<&mut dyn Future>,正是为了保证在整个轮询过程中 Future 不会被移动.
总结
| 概念 | 说明 |
|---|---|
Pin<P> | 包装指针,承诺不移动其指向的值 |
Unpin | 标记trait,表示该类型可以安全移动,Pin不限制它 |
!Unpin | 自引用结构(如async状态机)需要被Pin保护 |
| 使用场景 | 手写 Future、调用底层 poll 时需要处理 Pin |
💡 日常使用
async/await时,Pin由编译器和运行时自动处理,通常不需要手动介入.只有在手写Future或使用tokio::pin!宏固定栈上变量时才会直接接触到它.