Pin 是一个用于处理自引用结构(Self-referential structs)和异步编程的核心工具。它的核心作用是:**保证被包裹的值在内存中的地址不会发生改变(即"不可移动")**,从而防止因对象移动导致的悬垂指针问题。
Pin<P> 是一个包装类型,它保证被包装的指针 P 所指向的值不会被移动。具体来说,当我们拿到 Pin<&mut T> 或 Pin<Box<T>> 时,无法通过 safe 代码拿到 &mut T,从而无法使用 swap、replace 等会移动数据的方法,除非 T 实现了 Unpin。
12.1 Unpin 与 !Unpin
Unpin是一个自动 trait,几乎所有的标准类型(i32、String、Vec<T>等)都实现了它。实现了Unpin的类型,即使被Pin包裹,也可以安全地移动,Pin对它们没有任何限制。- 没有实现
Unpin的类型(通常用!Unpin表示)才是真正需要被"钉住"的类型。在Pin包裹下,它们无法被移动,从而保证了内部指针的稳定。
如何让一个类型变为 !Unpin?最简单的办法是加入一个 PhantomPinned 字段:
rust
use std::marker::PhantomPinned;
struct MyStruct {
data: String,
_pinned: PhantomPinned,
}
PhantomPinned 不实现 Unpin,因此包含它的结构体也不会自动实现 Unpin。
12.2 安全构造 Pin 的常见方式
12.2.1 Pin::new ------ 仅适用于 Unpin 类型
rust
let value = 42;
let pinned = Pin::new(&value); // value 是 i32,实现了 Unpin
对于 !Unpin 类型,Pin::new 无法使用,因为该方法要求 T: Unpin。
12.2.2 Box::pin ------ 将值固定在堆上(推荐)
rust
let mut pinned = Box::pin(MyStruct {
data: String::from("hello"),
_pinned: PhantomPinned,
});
这样 pinned 里的值就被固定在堆上,不会被移动。
12.2.3 Pin::new_unchecked ------ 不安全构造
rust
let mut value = MyStruct { data: String::from("hello"), _pinned: PhantomPinned };
let pinned = unsafe { Pin::new_unchecked(&mut value) };
需要开发者自己保证该值在 Pin 存在期间不会被移动,通常不推荐。
12.3 安全的自引用结构体
rust
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::ptr::NonNull;
struct SelfRefNode {
data: String,
// 指向 data 内部字节的指针
self_ref: Option<NonNull<u8>>, // 指向 data 内部字节的指针, 用于在 Pin 内部访问 data
_pinned: PhantomPinned, // 结构体实现 !Unpin
}
impl SelfRefNode {
/// 创建并固定一个自引用节点
pub fn new(data: String) -> Pin<Box<Self>> {
let mut boxed = Box::pin(SelfRefNode {
// 并将其固定到 Pin 中
data,
self_ref: None,
_pinned: PhantomPinned, // 结构体实现 !Unpin
});
// 在 Pin 的上下文中初始化自引用指针
let self_ptr: *const u8 = boxed.data.as_ptr(); // 获取 data 内部字节的指针
unsafe {
let mut_ref = Pin::as_mut(&mut boxed).get_unchecked_mut(); // 获取 Pin 内部的可变引用
mut_ref.self_ref = NonNull::new(self_ptr as *mut u8); // 初始化 self_ref 指向 data 内部字节的指针
}
boxed
}
/// 安全读取数据
pub fn data(self: Pin<&Self>) -> &str {
&self.get_ref().data // 安全读取 data 字段
}
/// 通过自引用指针读取数据,验证自引用的正确性
pub fn data_via_ref(self: Pin<&Self>) -> &str {
let this = self.get_ref(); // 获取 Pin 内部的引用
let ptr = this.self_ref.expect("指针未初始化"); // 获取 self_ref 指向的指针
unsafe {
let slice = std::slice::from_raw_parts(ptr.as_ptr(), this.data.len()); // 从指针创建一个原始切片
std::str::from_utf8(slice).unwrap() // 从原始切片创建一个字符串
}
}
}
fn main() {
let node = SelfRefNode::new("hello pin".to_string());
println!("直接读取: {}", node.as_ref().data());
println!("通过自引用指针读取: {}", node.as_ref().data_via_ref());
// 尝试移动 node 会导致编译错误,因为 Pin<Box<SelfRefNode>> 没有实现 Unpin
// let moved = node; // 错误!
}
12.4 异步编程中的 Pin 实例
Future trait 的 poll 方法签名是 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>。这让异步运行时可以安全地轮询一个可能具有自引用状态的 Future。
下面是一个简单的异步计时器,它内部需要跨 await 保持引用,因此必须使用 Pin。
rust
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
struct Timer {
end_time: Instant, // 定时器结束时间
started: bool, // 定时器是否已启动
}
impl Timer {
fn new(duration: Duration) -> Self {
Timer {
end_time: Instant::now() + duration, // 计算定时器结束时间
started: false, // 初始化定时器为未启动状态
}
}
}
impl Future for Timer {
// 实现 Future trait
type Output = (); // 定时器完成时返回的类型
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
// 实现 poll 方法
// 如果定时器未启动,启动定时器
if !self.started {
self.started = true; // 标记定时器为已启动
// 通知 waker,在指定时间后唤醒
let waker = cx.waker().clone(); // 克隆 waker,确保在子线程中唤醒
let end_time = self.end_time; // 记录定时器结束时间
std::thread::spawn(move || {
std::thread::sleep(end_time - Instant::now()); // 等待定时器结束时间
waker.wake(); // 唤醒任务
});
Poll::Pending // 定时器未到,返回 Pending
} else if Instant::now() >= self.end_time {
Poll::Ready(()) // 定时器到,返回 Ready(())
} else {
Poll::Pending // 定时器未到,返回 Pending
}
}
}
// 使用示例
#[tokio::main]
async fn main() {
let timer = Timer::new(Duration::from_secs(2)); // 创建一个 2 秒的定时器
timer.await; // 等待定时器到
println!("2 秒后打印");
}
在这个例子中,Timer 虽然本身不包含自引用,但它的 poll 方法接收 Pin<&mut Self>,符合 Future 规范。对于那些包含跨 await 引用的 Future,Pin 会保证它们在轮询过程中不被移动,从而保护内部指针安全。
12.5 总结
Pin解决的是自引用结构体因移动导致指针悬垂的问题,在异步编程中尤为关键。Unpin类型可以安全移动,Pin对它们透明;!Unpin类型在Pin包裹下被禁止移动。- 构造
Pin的常用方式是Box::pin(堆上固定)或Pin::new_unchecked(需自行保证安全)。 - 在
Future的实现中,poll方法接收Pin<&mut Self>,保证了状态机内部跨await引用的有效性。
通过上面这些机制和实例,你应该对 Pin 的设计意图和实际用法有了比较清晰的认识。