引言
这是Rust九九八十一难第13篇,介绍下pin指针。pin指针跟Future紧密相关,也算是多线程部分第三篇。关于这块,之前看了几篇文章,有评论这块难以理解,我也同感。忘记谁说的了,如果不能简单明了的描述某个东西,说明自己还没真正掌握。本篇尝试用简单方式整理Pin相关的知识,有问题请留言。
一、基本概念
入门pin,先要知道四个概念:pin,Unpin和!Upin,以及rust的move。通过这些概念的对比,理解pin的边界,在哪些范围发生作用。
1、Pin<T> 到底是什么
Pin是个智能指针包装器。比如:
1 | use std::pin::Pin; |
- 它包裹了一个类型
T(通常是放在堆上的,如Box<T>),对 !Unpin 类型 , 编译器会在语法层面阻止移动。 - 简单说就是可以拿到
&T或&mut T,但保证不会把整个T移到别的内存位置。
2、Unpin是什么?
Unpin 是一个 标记 trait,表示该类型可以安全地被移动。
大多数普通类型(如
i32,String,Vec<T>)默认实现了Unpin;但一些类型(如
Future、自引用结构)不会自动实现。1
2
3fn need_unpin<T: Unpin>(x: T) {
println!("可以安全移动");
}如果某类型没有
Unpin,那么它就 必须被固定(pinned) 才能安全使用。
3、!Unpin是什么
!Unpin 就是 没有实现 Unpin 的类型,表示类型 不能随意移动。!Unpin 并不是 Rust 的语法,而是“没有实现 Unpin 的类型”的意思。Rust 默认会自动为大部分类型实现 Unpin,只有少数类型(自引用、Future、PhantomPinned)才是 !Unpin
1 | use std::pin::Pin; |
Unpin 类型 → 钉住也能搬,!Unpin 类型 → 钉住后无法搬。换句话**!Unpin 的作用是“固定对象的地址”,而不是搬动堆上的数据**,如下图:
Stack Heap
+-------+ +--------+
| a | --------------> | "hello"|
| Box | +--------+
+-------+
!Unpin 阻止 a 被 move
a(栈上的 Box)被 Pin , 栈上的地址不能被移动,堆上的 "hello"数据不受影响Pin 作用在 栈上的对象地址,确保内部指针不会悬空
4、Pin、Unpin和!Unpin三者对比
| 类型 | Pin 是否生效 | 说明 |
|---|---|---|
| Unpin | ❌ 不生效(透明) | 移动仍然允许,安全无问题 |
| !Unpin | ✅ 生效 | 栈上值被固定,移动会编译报错,保护内部引用安全 |
| Copy 类型 | ❌ 不生效 | move 其实是复制,Pin 对它无意义 |
Pin 是动作,“把对象钉住”,Unpin / !Unpin 决定钉住后能否移动。
5、Pin与move的关系
| 类型 | Copy? | Unpin? | Pin 后移动? | Pin 作用 |
|---|---|---|---|---|
| i32 / bool / f64 | ✅ | ✅ | ✅ 可以移动 | 不起作用(透明) |
| String / Vec / Box | ❌ | ✅ | ✅ 可以移动 | 对 Unpin 类型不起作用 |
| 自引用 / Future | ❌ | ❌ | ❌ 禁止移动 | Pin 生效,保护内部引用 |
Pin 的实际意义只针对 !Unpin (Pin<T: !Unpin>)类型,Copy 或 Unpin 类型( String、Vec、Box) 标记了 Pin(Pin<T: Unpin>),本质上不起作用。
二、核心API使用入门
上一章节确定了边界,这一章看下Pin的api怎么用。
示例1:Pin<Box>
1 | use std::pin::Pin; |
说明:
Pin<Box<T>>保证T在堆上的地址不会改变;仍然可以修改内容,但不能“移走”整个结构体。- Pin::new():对
!Unpin类型(例如自引用结构体),不能直接用这个函数。编译器会强制你使用unsafe { Pin::new_unchecked(...) }。 - 如果想只有当类型是
Unpin(可安全移动)时才能用into_inner。
示例2:自引用结构体(Pin 的典型场景)
1 | use std::pin::Pin; |
示例 3:在异步任务中(Future 的典型应用)
async fn 生成的状态机其实是 自引用结构体。 Pin 在运行时保护它不被移动。
1 | use std::pin::Pin; |
Pin 确保 MyFuture 内部状态(如引用)不会在 .await 期间被移动。这就是为什么 async 语法能安全地处理复杂状态。
示例4:unsafe
a) Pin::new_unchecked
1 | let mut x = SelfRef { ... }; // !Unpin 类型 |
- 为什么 unsafe:
- 编译器不能保证后续不会移动
x - 需要开发者保证
x在生命周期内不会移动
- 编译器不能保证后续不会移动
- Pin::new_unchecked:必须 100% 确保 这个值不会在 pinned 后被移动,一般只在底层框架(如 tokio、futures)或自引用实现中用,业务代码一般不用。
b) Pin::get_unchecked_mut
1 | let mut px: Pin<&mut SelfRef> = Pin::new(&mut x); |
- get_unchecked_mut():不安全适用任何类型,跳过移动安全检查,手动保证 pinned 值不会移动
c) into_inner 堆上对象拆回原类型(!Unpin 类型不可行)
1 | let px: Pin<Box<SelfRef>> = Box::pin(SelfRef { ... }); |
- 对 Unpin 类型安全,可以拆回 Box
- 对 !Unpin 类型,拆回 Box 需要 unsafe 并自己保证移动不会破坏安全(一般不推荐)
三、为什么要自引用
pin指针常用场景是自引用,这里聊聊自引用是啥,为什么有自引用。
1、对比方法访问和内部引用
假设我们有一个字符串字段 data,想访问前两个字符:
a、方法访问(推荐做法)
1 | struct MyStruct { |
优点:
Rust borrow checker 安全,没有悬空指针,简单、可维护缺点:
每次调用都会生成一个切片(非常轻量级,但在高性能/大量数据场景下可能产生微小开销)
b、内部引用(self_ref / slice 指向 data)
1 | struct SelfRef { |
优点:
- 零拷贝:切片预先计算好,访问不需要每次切分
- 可用于异步/自引用结构,避免在 Future 状态机 poll 时重复生成切片
缺点:
- 必须使用 Pin 或堆分配保证
data地址不变 - 使用裸指针,需要
unsafe,风险大 - 程序复杂度高,可维护性差
- 必须使用 Pin 或堆分配保证
2、自引用场景
a、零拷贝解析
HTTP、JSON、CSV、文本流等大量数据处理,想存储对 buffer 的切片,而不是复制字符串。那内部引用可以直接保存 slice,避免每次 data[0..n] 生成新切片
b、异步状态机 / Future 自引用
状态机字段之间可能互相引用Poll 时不希望重新计算 slice 或临时变量
c、高性能图结构 / AI / Tensor
节点存指针指向数据的一部分,而不是每次生成新对象
3、不加pin的有什么问题
假设2的场景没加pin,Rust 编译器在面对“直接自引用”,通常会:
如果是安全引用(
&str),直接拒绝编译;1
2
3
4
5
6
7
8
9
10
11
12
13
14struct BadRef<'a> {
data: String,
slice: &'a str, // 引用自身字段
}
impl<'a> BadRef<'a> {
fn new(txt: &str) -> Self {
let s = txt.to_string();
Self {
data: s,
slice: &s, // ❌ 编译错误
}
}
}error[E0505]: cannot move out of `s` because it is borrowed --> src/main.rs:9:13 | 8 | let s = txt.to_string(); | - binding `s` declared here 9 | Self { data: s, slice: &s } | ^ move out of `s` occurs here | | | borrow later used hereRust 检测到
&s引用了局部变量s,但又 move 了它(所有权转移),这违反了生命周期规则。如果用裸指针(
*const T),编译能过但属于未定义行为(UB)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29use std::ptr;
struct SelfRef {
data: String,
ptr: *const String,
}
impl SelfRef {
fn new(s: &str) -> Self {
let data = String::from(s);
let ptr = &data as *const String;
Self { data, ptr }
}
fn print(&self) {
unsafe {
// 访问 ptr 指向的旧地址
println!("ptr -> {}", &*self.ptr);
}
}
}
fn main() {
let x = SelfRef::new("hello");
let mut y = x; // move 发生
// 此时 y.ptr 仍指向 x.data 的旧地址(已被 drop)
y.print(); // ❌ UB: 访问已释放内存
}interrupted by signal 11:SIGSEGV,或者出现乱码,因为
b.slice指向了a.data的旧位置。有的rust版本可能不崩,有点随机,所以是未定义行为。
四、Pin具体是怎么固定的
Pin 本身不保证对象真的“不会被移动”,它只是 在类型系统层面限制移动,依赖于“不能获取 &mut T 原始引用:
- 对于
Pin<Box<T>>,堆上分配 + 无法替换指针 = 地址稳定 - 对于
Pin<&mut T>,编译器禁止T被mem::replace()或move
1、PhantomPinned + !Unpin
Rust 编译器通过 类型系统约束固定内部指针。PhantomPinned 用来标记一个类型 不可移动。默认情况下,所有类型都实现 Unpin:
Unpin意味着可以安全移动。自引用类型必须显式禁用
Unpin(通过PhantomPinned)。1
2
3
4
5
6
7
8use std::marker::PhantomPinned;
use std::pin::Pin;
struct SelfRef {
data: String,
ptr: *const String,
_pin: PhantomPinned, // 禁止移动
}
2、Pin 的 API 层约束
Pin的核心api定义:
1 | impl<T: ?Sized> Pin<&mut T> { |
as_mut安全获取可变引用,但仍然被 Pin 约束。get_unchecked_mut是 unsafe的,允许手动移动内部字段,但风险自担。编译器只允许安全 API 移动外部包裹指针,但内部 T 地址固定。
3、阻止move的例子
堆上移动禁止的例子,Pin在编译期就会阻止
1 | use std::pin::Pin; |
原理:
- Box 是智能指针,移动 Box 只移动指针,不移动堆数据,如下图,堆没变;
- Pin API 保证 T 在堆上的内存地址不会改变。
五、为什么 Future 要有 Pin 设计
1、future自引用
一个 async fn 编译后其实会生成一个状态机结构体
1 | async fn foo() { |
编译器大致会生成:
1 | enum FooFuture { |
每次 .poll() 时,Future 会被推进到下一个状态。
在 State1 中,bar_fut 持有一个对 s 的引用:BarFuture<'_> // 生命周期依赖 FooFuture::s
这意味着整个 FooFuture 结构体内部出现了**自引用(self-referential)**关系:
FooFuture
├─ s: String
└─ bar_fut: BarFuture<'s>
↑
└── 引用了 s
2、Future何时被移动
当 Future 被运行(比如通过 tokio::spawn、block_on)时,执行器会做这样的事:
1 | let fut = foo(); // 创建 Future |
在任务系统中,执行器通常会:
- 把
fut移进某个堆上分配的任务结构体; - 再从任务结构体中取出
Pin<&mut fut>去调用poll()。
这样,在被 poll 之前,它已经被 move 过一次了。
更隐蔽的情况:
1 | let fut = foo(); |
fut 被嵌入另一个 Future f1 中。 f1 也会被 executor 再次移动。 也就是说 fut 可能经历:
1 | foo() -> 创建 Future |
如果 FooFuture 在 poll 过程中被 移动 (move),那么 s 在内存中的地址就变了。
但 bar_fut 仍然保存着旧地址的引用,则引用失效,属于UB(未定义行为)!。
所以Rust通过Pin来固定Future内存位置
3、流程如下
说明:
- 当写一个
async fn foo() { … }或async { … },Rust 编译器把它转成一个状态机 struct,其字段包括局部变量、状态标记、可能的 Waker 等。 - 如果这个状态机 在 await 点之后 还持有对自身结构体内部字段(例如
&mut self.field、或&self.field)的引用,那么它就是一个 自引用 Future。也就是说,它存储了指向自身内容的引用。 - 若这种 Future 被移动(即地址变化),那么这些内部引用就变成悬垂引用,可能引起 UB。
- 因此,为了安全,Rust 要求:若一个 Future 有可能是自引用的(即生成器状态机可能这样),那么它必须 先固定住地址(pinned),再被
poll。这就是为什么poll(self: Pin<&mut Self>, …)而不是&mut Self。https://stackoverflow.com/questions/72769316/why-do-futures-use-pins-in-rust?utm_source=chatgpt.com - 在
Pin<&mut Self>的约束下,类型系统禁止你再偷偷将 Self 移动。只有当 Self 类型实现了Unpin(表示“即便被 Pin 包装,也可安全移动”)时,才允许“解除固定”的操作。 - 总结来说,在 poll 期间,保证 Future 所表示的内存位置不会改变**,从而底层状态机字段中的“self-引用”依然有效。
六、总结
Pin主要保证 !Unpin 类型在内存中的地址固定,比如自引用类型、异步 Future 类型(async/await 内部状态机)、底层异步 I/O 驱动(如 Tokio 内部 task、io_uring buffer)。理解起来有点复杂,对普通类型或短生命周期变量没必要使用,除非是明确的需求。如果用的话,能用安全 API 就不用 unsafe;只能 unsafe 的地方,要保证对象绝对不动。
如果觉得有用,请点个关注吧。