引言
这是Rust九九八十一难第13篇,介绍下pin指针。pin指针跟Future紧密相关,也算是多线程部分第三篇。关于这块,之前看了几篇文章,有评论这块难以理解,我也同感。忘记谁说的了,如果不能简单明了的描述某个东西,说明自己还没真正掌握。本篇尝试用简单方式整理Pin相关的知识,有问题请留言。
一、基本概念
入门pin,先要知道四个概念:pin,Unpin和!Upin,以及rust的move。通过这些概念的对比,理解pin的边界,在哪些范围发生作用。
1、Pin<T> 到底是什么
Pin是个智能指针包装器。比如:
rust
use std::pin::Pin;
fn main() {
let x = 10;
let mut pinned = Pin::new(&x);
println!("Pinned: {:?}", pinned);//Pinned: 10
}
- 它包裹了一个类型
T(通常是放在堆上 的,如Box<T>),对 !Unpin 类型 , 编译器会在语法层面阻止移动。 - 简单说就是可以拿到
&T或&mut T,但保证不会把整个T移到别的内存位置。
2、Unpin是什么?
Unpin 是一个 标记 trait,表示该类型可以安全地被移动。
-
大多数普通类型(如
i32,String,Vec<T>)默认实现 了Unpin; -
但一些类型(如
Future、自引用结构)不会自动实现。rustfn need_unpin<T: Unpin>(x: T) { println!("可以安全移动"); }如果某类型没有
Unpin,那么它就 必须被固定(pinned) 才能安全使用。
3、!Unpin是什么
!Unpin 就是 没有实现 Unpin 的类型 ,表示类型 不能随意移动 。!Unpin 并不是 Rust 的语法,而是"没有实现 Unpin 的类型"的意思。Rust 默认会自动为大部分类型实现 Unpin,只有少数类型(自引用、Future、PhantomPinned)才是 !Unpin
rust
use std::pin::Pin;
use std::marker::PhantomPinned;
struct SelfRef {
data: String,
ptr: *const String,
_pin: PhantomPinned, // 表示 !Unpin
}
fn main() {
// 普通类型 Unpin
let x = Box::new(42);
let px = Pin::new(x);
let moved_px = px; // ✅ 可以移动
// 自引用类型 !Unpin
let y = Box::pin(SelfRef {
data: "hello".to_string(),
ptr: std::ptr::null(),
_pin: PhantomPinned,
});
// let moved_y = y; // ❌ 编译报错,不可移动
}
Unpin 类型 → 钉住也能搬,!Unpin 类型 → 钉住后无法搬。换句话**!Unpin 的作用是"固定对象的地址",而不是搬动堆上的数据**,如下图:
lua
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>
rust
use std::pin::Pin;
struct Data {
value: String,
}
fn main() {
let boxed = Box::new(123);
let pinned = Pin::new(boxed);
// i32 是 Unpin,所以可以安全取出
let inner = Pin::into_inner(pinned);
println!("{}", inner);
let mut data = Data {
value: "hello".to_string(),
};
// 普通 Box,可以自由移动
let mut boxed = Box::new(data);
// Pin<Box<T>>:禁止移动内部 T
let mut pinned: Pin<Box<Data>> = Pin::new(boxed);
// 安全访问字段
println!("Value = {}", pinned.value);
// 尝试移动 pinned(编译失败),内部值(Data)被移动出原来的内存地址,原先地址上的那块堆内存变成空的,违反了pin的约束
// let moved = *pinned; // ❌
}
说明:
Pin<Box<T>>保证T在堆上的地址不会改变;仍然可以修改内容,但不能"移走"整个结构体。- Pin::new():对
!Unpin类型(例如自引用结构体),不能直接用这个函数。编译器会强制你使用unsafe { Pin::new_unchecked(...) }。 - 如果想只有当类型是
Unpin(可安全移动)时才能用into_inner。
示例2:自引用结构体(Pin 的典型场景)
rust
use std::pin::Pin;
use std::ptr::NonNull;
struct SelfRef {
data: String,
ptr: Option<NonNull<String>>,
}
impl SelfRef {
fn new(txt: &str) -> SelfRef {
SelfRef {
data: txt.to_string(),
ptr: None,
}
}
fn init(self: Pin<&mut SelfRef>) {
// 安全地拿到内部可变引用
let this = unsafe { self.get_unchecked_mut() };
// 设置指针指向自己字段
this.ptr = Some(NonNull::from(&this.data));
}
fn print(&self) {
unsafe {
println!("self.data = {}", self.data);
if let Some(ptr) = self.ptr {
println!("ptr -> {}", ptr.as_ref());
}
}
}
}
fn main() {
let mut s = Box::pin(SelfRef::new("Rust Pin!"));
s.as_mut().init(); // 初始化自引用
s.print(); // ✅ OK:data 没被移动
}
示例 3:在异步任务中(Future 的典型应用)
async fn 生成的状态机其实是 自引用结构体 。 Pin 在运行时保护它不被移动。
rust
use std::pin::Pin;
use std::future::Future;
use std::task::{Context, Poll};
struct MyFuture {
counter: u8,
}
impl Future for MyFuture {
type Output = u8;
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.counter < 3 {
self.counter += 1;
println!("Counting: {}", self.counter);
Poll::Pending
} else {
Poll::Ready(self.counter)
}
}
}
#[tokio::main]
async fn main() {
let result = MyFuture { counter: 0 }.await;
println!("Done: {}", result);
}
Pin 确保 MyFuture 内部状态(如引用)不会在 .await 期间被移动。这就是为什么 async 语法能安全地处理复杂状态。
示例4:unsafe
a) Pin::new_unchecked
rust
let mut x = SelfRef { ... }; // !Unpin 类型
let px = unsafe { Pin::new_unchecked(&mut x) };
- 为什么 unsafe :
- 编译器不能保证后续不会移动
x - 需要开发者保证
x在生命周期内不会移动
- 编译器不能保证后续不会移动
- Pin::new_unchecked:必须 100% 确保 这个值不会在 pinned 后被移动,一般只在底层框架(如 tokio、futures)或自引用实现中用,业务代码一般不用。
b) Pin::get_unchecked_mut
rust
let mut px: Pin<&mut SelfRef> = Pin::new(&mut x);
unsafe {
let mut_ref: &mut SelfRef = Pin::get_unchecked_mut(px.as_mut());
mut_ref.ptr = &mut mut_ref.data; // 自引用赋值
}
- get_unchecked_mut():不安全适用任何类型,跳过移动安全检查,手动保证 pinned 值不会移动
c) into_inner 堆上对象拆回原类型(!Unpin 类型不可行)
rust
let px: Pin<Box<SelfRef>> = Box::pin(SelfRef { ... });
// let b: Box<SelfRef> = Pin::into_inner(px); // ❌ 不安全,编译禁止
- 对 Unpin 类型安全,可以拆回 Box
- 对 !Unpin 类型,拆回 Box 需要 unsafe 并自己保证移动不会破坏安全(一般不推荐)
三、为什么要自引用
pin指针常用场景是自引用,这里聊聊自引用是啥,为什么有自引用。
1、对比方法访问和内部引用
假设我们有一个字符串字段 data,想访问前两个字符:
a、方法访问(推荐做法)
rust
struct MyStruct {
data: String,
}
impl MyStruct {
fn slice(&self) -> &str {
&self.data[0..2] // 每次调用都生成切片
}
}
fn main() {
let s = MyStruct { data: "Hello".into() };
println!("{}", s.slice()); // "He"
}
-
优点: Rust borrow checker 安全,没有悬空指针,简单、可维护
-
缺点: 每次调用都会生成一个切片(非常轻量级,但在高性能/大量数据场景下可能产生微小开销)
b、内部引用(self_ref / slice 指向 data)
rust
struct SelfRef {
data: String,
slice: *const str, // 内部指针
}
impl SelfRef {
fn new(txt: &str) -> Self {
let mut s = SelfRef {
data: txt.to_string(),
slice: std::ptr::null(),
};
s.slice = &s.data[0..2] as *const str; // 指向 data
s
}
fn get_slice(&self) -> &str {
unsafe { &*self.slice }
}
}
-
优点:
- 零拷贝:切片预先计算好,访问不需要每次切分
- 可用于异步/自引用结构,避免在 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),直接拒绝编译;ruststruct 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, // ❌ 编译错误 } } }swifterror[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)。rustuse 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)。rustuse std::marker::PhantomPinned; use std::pin::Pin; struct SelfRef { data: String, ptr: *const String, _pin: PhantomPinned, // 禁止移动 }
2、Pin 的 API 层约束
Pin的核心api定义:
rust
impl<T: ?Sized> Pin<&mut T> {
pub fn as_mut(self: Pin<&mut T>) -> Pin<&mut T> { ... }
pub unsafe fn get_unchecked_mut(self: Pin<&mut T>) -> &mut T { ... }
}
-
as_mut安全获取可变引用,但仍然被 Pin 约束。 -
get_unchecked_mut是 unsafe的,允许手动移动内部字段,但风险自担。 -
编译器只允许安全 API 移动外部包裹指针,但内部 T 地址固定。
3、阻止move的例子
堆上移动禁止的例子,Pin在编译期就会阻止
rust
use std::pin::Pin;
use std::marker::PhantomPinned;
struct SelfRef {
data: String,
ptr: *const String,
_pin: PhantomPinned, // !Unpin,禁止移动
}
fn main() {
// 堆上创建 SelfRef 并 Pin
let mut boxed: Pin<Box<SelfRef>> = Box::pin(SelfRef {
data: String::from("hello"),
ptr: std::ptr::null(),
_pin: PhantomPinned,
});
// 初始化内部自引用指针
let ptr = &boxed.data as *const String;
unsafe {
let mut_ref = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).ptr = ptr;
}
// ❌ 尝试 move 内部 SelfRef 会报错
let moved = *boxed; // error: cannot move out of `*boxed` because it is pinned
// // 但是 Box 本身可以移动
// let moved_boxed = boxed; // ✅ Box 可移动,但堆上地址固定
//
// println!("data via pinned: {}", unsafe { &*moved_boxed.ptr });
}
原理:
- Box 是智能指针,移动 Box 只移动指针,不移动堆数据,如下图,堆没变;
- Pin API 保证 T 在堆上的内存地址不会改变。

五、为什么 Future 要有 Pin 设计
1、future自引用
一个 async fn 编译后其实会生成一个状态机结构体
rust
async fn foo() {
let s = String::from("hello");
bar(&s).await;
println!("{s}");
}
编译器大致会生成:
rust
enum FooFuture {
State0, // 初始状态
State1 { s: String, bar_fut: BarFuture<'_> }, // await 之后
Done,
}
每次 .poll() 时,Future 会被推进到下一个状态。
在 State1 中,bar_fut 持有一个对 s 的引用:BarFuture<'_> // 生命周期依赖 FooFuture::s
这意味着整个 FooFuture 结构体内部出现了**自引用(self-referential)**关系:
rust
FooFuture
├─ s: String
└─ bar_fut: BarFuture<'s>
↑
└── 引用了 s
2、Future何时被移动
当 Future 被运行(比如通过 tokio::spawn、block_on)时,执行器会做这样的事:
rust
let fut = foo(); // 创建 Future
executor.spawn(fut); // 把 Future 移进任务队列(Move #1)
在任务系统中,执行器通常会:
- 把
fut移进某个堆上分配的任务结构体; - 再从任务结构体中取出
Pin<&mut fut>去调用poll()。
这样,在被 poll 之前,它已经被 move 过一次了。
更隐蔽的情况:
rust
let fut = foo();
let f1 = async { fut.await };
fut 被嵌入另一个 Future f1 中。 f1 也会被 executor 再次移动。 也就是说 fut 可能经历:
rust
foo() -> 创建 Future
↓ move
async { fut.await } -> 另一个 Future 包装
↓ move
executor.spawn() -> 放入任务
↓ move
poll() -> 固定到堆上
如果 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。stackoverflow.com/questions/7... - 在
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 的地方,要保证对象绝对不动。
如果觉得有用,请点个关注吧。