一、自引用类型
很多人知道自引用类型,那么这究竟是什么概念呢,其实就是以下的这个结构体:
struct SelfReferential {
data: String,
ptr: *const String, // 指向 data 的指针
}
在这里我们定义了一个SelfReferential的结构体,里面有个data和ptr,而且要求ptr是指向的data。
请看下面的main方法:
fn main() {
let mut instance = SelfReferential {
data: String::from("Hello, world!"),
ptr: std::ptr::null(), // 初始化为 null
};
instance.ptr = &instance.data; // 将 ptr 指向 data
println!("--- before move ---");
println!("instance address: {:p}", &instance.data);
println!("instance pointer: {:p}", instance.ptr);
let moved_instance = instance;
println!("--- after move ---");
println!("moved instance address: {:p}", &moved_instance.data);
println!("moved instance pointer: {:p}", moved_instance.ptr);
}
我们把instance的所有权,转移到了moved_instance,此时我们打印一下输出的地址的值:
--- before move ---
instance address: 0x16f462040
instance pointer: 0x16f462040
--- after move ---
moved instance address: 0x16f4620c0
moved instance pointer: 0x16f462040
可以看到在after move之后,整个结构体已经不再是自引用类型了。
Pin保证自引用类型特征
use std::pin::Pin;
use std::marker::PhantomPinned;
struct SelfReferential {
data: String,
ptr: *const String,
_pin: PhantomPinned, // 核心改动 1:加上这个标记,禁用 Unpin
}
fn main() {
// 核心改动 2:用 Box::pin 把数据钉在堆上,拿到一个 Pin 过的指针
let mut instance: Pin<Box<SelfReferential>> = Box::pin(SelfReferential {
data: String::from("Hello, world!"),
ptr: std::ptr::null(),
_pin: PhantomPinned,
});
// 核心改动 3:使用 unsafe 拿到内部的可变引用来设置指针
// 只有在初始化这一下需要 unsafe,因为此时你必须保证地址不再变
let data_ptr = &instance.data as *const String;
unsafe {
instance.as_mut().get_unchecked_mut().ptr = data_ptr;
}
println!("--- before move ---");
println!("instance address: {:p}", &instance.data);
println!("instance pointer: {:p}", instance.ptr);
// 此时,你无法再像之前那样 `let moved = *instance;`
// 因为 PhantomPinned 阻止了解引用移动
let moved_instance: Pin<Box<SelfReferential>> = instance;
println!("--- after move ---");
println!("moved instance address: {:p}", &moved_instance.data);
println!("moved instance pointer: {:p}", moved_instance.ptr);
}
加入了 _pin: PhantomPinned 后,这个结构体变成了 !Unpin。这意味着 Rust 编译器会拒绝执行任何可能导致它在内存中移动的安全代码。
请看输出:
--- before move ---
instance address: 0x13be05d80
instance pointer: 0x13be05d80
--- after move ---
moved instance address: 0x13be05d80
moved instance pointer: 0x13be05d80
可以看到,改进后 after move 的两个地址会完全一致。
二、Future和自引用结构
其实 Rust 引入 Pin 的头号动机,根本不是为了让你手动写自引用结构体,而是为了让 async/await 能跑起来。
写的这个 SelfReferential 结构体,本质上就是 Future 在被编译后生成的状态机(State Machine)的缩影。
1. 为什么 Future 需要 Pin?
当你写一段异步代码时,编译器会把它变成一个隐藏的结构体:
async fn my_async_fn() {
let mut data = String::from("Hello"); // 局部变量
let ptr = &data; // 跨越 await 的引用
some_other_await().await;
println!("{}", ptr);
}
编译器生成的“状态机”结构体大概长这样:
struct MyAsyncFuture {
data: String,
ptr: *const String, // 这个指针指向了同一个结构体内部的 data!
state: State,
}
看出来了吗?每一个含有跨越 await 引用的 async 函数,在底层都是一个自引用结构体。
如果这个 Future 在 await 期间被 mem::swap 或者从一个线程移动到了另一个线程,它的内部指针(ptr)就会失效。
2. 在 Tokio/Future 中 Pin 是怎么起作用的?
当你调用 future.poll(cx) 时,你会发现 poll 方法的签名是:
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>
它强制要求 self 必须被 Pin 住。
-
执行器的保证:像 Tokio 这样的执行器(Runtime)在调用你的
poll之前,会确保这个Future已经被固定在内存里(通常是通过Box::pin或者在栈上pin_mut!)。 -
安全的移动:只要
Future被Pin住了,Tokio 就可以放心地在不同的任务之间切换,即便这个Future内部有再复杂的自引用指针,地址也不会变。
3. 用你的例子类比 Tokio 的操作
我们可以把你的 main 函数想象成一个简单的 Tokio 调度器:
-
准备阶段:
Box::pin(...)。Tokio 接收到一个新的任务,把它放进堆里并固定。 -
执行阶段:
instance.as_mut().get_unchecked_mut()。Tokio 调用poll方法。在poll内部,状态机可以安全地利用自引用指针来访问之前暂存的变量(比如data)。 -
暂停与恢复:
let moved_instance = instance;。在await挂起期间,虽然存储Pin的“盒子”在执行器队列里可能被移动了位置,但盒子里的东西(你的data)没动。
4. 为什么有些 Future 不需要 Pin (Unpin)?
你代码里的 _pin: PhantomPinned 就像是给结构体上了锁。
但在 Rust 中,像 i32、String 甚至不含引用的普通结构体,都是 Unpin 的。
- 如果你的 Future 没引用自己:那么它即使被移来移去也无所谓。
-
如果你在用 Tokio 的某些组件:比如
tokio::sync::mpsc,你会发现有些类型不需要Pin也能工作。
三、tokio::pin!的用法场景
我们来聊聊 tokio::pin!。如果说 Box::pin 是把 Future 关进“堆里的保险箱”,那么 tokio::pin! 就是在“栈上打地钉”。
在 Tokio 中,你并不总是想用 Box::pin。因为 Box 会带来堆分配开销,对于高性能组件(比如你开发的 PalLink 网关或 AcorusDB),我们更倾向于栈固定。
1. 核心作用:将变量转换为 Pin<&mut T>
tokio::pin!(x) 宏会通过同名遮蔽(Shadowing),把一个普通变量 x 变成一个类型为 Pin<&mut T> 的固定引用,且保证它在当前作用域内不可移动。
2. 场景一:在 loop 和 select! 中复用 Future
这是最经典、最高频的场景。如果你直接在 select! 里使用一个 Future 的引用,它在每次循环时都会尝试重新 Move。通过 pin! 固定后,才能安全地多次监听。
let fut = my_async_fn();
tokio::pin!(fut); // 将 fut 钉在栈上
loop {
tokio::select! {
_ = &mut fut => {
break; // Future 完成,退出循环
}
_ = tokio::time::sleep(Duration::from_secs(1)) => {
println!("Future 还没好,我等一秒再来问...");
// 因为 fut 被 pin 住了,所以它的内部自引用指针依然有效
// 下一次循环 poll 时不会崩
}
}
}
3. 场景二:手动调用 poll 方法
当你编写底层组件(比如实现一个自定义的 Stream 或 Future 代理)需要手动触发 poll 时,poll 的签名强制要求 Pin<&mut Self>。
let mut my_fut = my_async_fn();
tokio::pin!(my_fut);
// 只有 pin 之后,才能调用 poll
let res = my_fut.as_mut().poll(cx);
4. 场景三:避免堆分配 (Zero-cost)
-
Box::pin: 申请堆内存 → 拷贝数据进去 → 得到Pin<Box<T>>。 -
tokio::pin!: 就在当前栈位置原地打钉 → 得到Pin<&mut T>。
结论: 如果你的 Future 不需要跨越函数返回(只在当前函数内被 await 或处理),请永远优先使用 tokio::pin!。
💡 总结对比表
| 维度 | Box::pin(x) |
tokio::pin!(x) |
|---|---|---|
| 内存位置 | 堆 (Heap) | 栈 (Stack) |
| 开销 | 有申请/释放内存的开销 | 零开销 |
| 灵活性 | 高(可以作为返回值返回) | 低(只能在当前作用域使用) |
| 适用场景 | 动态创建 Task、跨函数传递 |
select! 循环、局部手动 Poll |