
Rust Pin 与自引用结构为什么 Future 需要钉在原地标签技术、所有权、Rust、Pin、Future、内存安全一、一个编译错误引发的血案关键词是这几个Pin、Unpin、self-referential、cannot move。我当时很崩溃我只是想写个异步函数怎么牵扯出这么多高级概念但后来我发现理解 Pin 的关键不在 Pin 本身而在为什么要发明 Pin 这个东西。这篇文章我想用大白话加画图的方式把我对 Pin 的理解分享出来。我不是 Rust 专家讲的可能是入门级理解欢迎指正。二、问题的根源自引用结构一切的起点是一个很朴素的需求我想在一个结构体里保存对自身某个字段的引用。// 这段代码不能编译只是为了说明想做什么 struct SelfReferential { data: String, // 实际数据 // 下面这行不能编译Rust 不允许结构体字段引用自身 // ptr: self.data, // 指向 data 的引用 }为什么 Rust 不允许因为当一个值被移动move时它的内存位置会变。如果有一个指向旧位置的指针移动后就变成悬垂指针了。flowchart LR subgraph Before[移动前结构体在地址 0x1000] B1[data: \hello\\n地址 0x1000] B2[ptr: → 0x1000 ✅] end subgraph After[移动后结构体被 memcpy 到 0x2000] A1[data: \hello\\n地址 0x2000] A2[ptr: → 0x1000 \n依然指向旧地址] end Before --|memcpy| After style B2 fill:#c8e6c9 style A2 fill:#ffcdd2图上很清晰结构体从地址 0x1000 移动到了 0x2000data 字段的内容也跟着搬过去了。但是ptr这个指针字段还指向旧地址 0x1000那地方的数据可能已经被覆盖了。这就是自引用结构在移动时产生的悬垂指针问题。可问题是Future异步任务本质上就是一种自引用结构。因为编译器在生成 Future 的状态机时会把局部变量和跨.await的引用放在同一个结构体里。三、Future 为什么是自引用的写一段简单的异步代码看看编译器生成了什么use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; // 我们的异步函数读文件 处理内容 async fn read_and_process(path: str) - String { // 第一步异步读取文件 let content tokio::fs::read_to_string(path).await.unwrap(); // 第二步拿到内容后做一些处理 // content 是第一步 .await 的返回值存放在 Future 的状态里 let processed content.to_uppercase(); // 第三步再异步写回去 tokio::fs::write(output.txt, processed).await.unwrap(); // processed 是 String它引用的是 Future 内部的 content 字段 // 所以这个 Future 就是一个自引用结构 processed }编译器生成的匿名 Future 类型大概长这样// 这是编译器生成的状态机简化版实际类型是匿名的 enum ReadAndProcessFuturea { // 状态 0还没开始执行 Start { path: a str }, // 状态 1正在等待 read_to_string 完成 // read_future 内部保存了 path 的副本 Reading { read_future: tokio::fs::ReadToStringFuture }, // 状态 2read_to_string 完成了正在处理 // content 是第一步 .await 的结果存放在这里 // write_future 引用 content // ↓↓↓ 这就是自引用 Processing { content: String, // 数据 write_future: tokio::fs::WriteFileFuture???, // 引用 content }, // 状态 3全部完成 Done, }看到Processing这个变体里的write_future引用了content没有这就是自引用结构。content和指向它的引用在同一个枚举变体里。问题是Future 可能会被移动比如你用Box::pin之前或者在一些特殊场景下。一移动write_future里的引用就成悬垂指针了。sequenceDiagram participant Code as 用户代码 participant Compiler as Rust 编译器 participant Future as 生成的 Future 状态机 Code-Compiler: async fn read_and_process() Compiler-Future: 生成匿名 Future 类型 Note over Future: 状态 1: Reading Future-Future: read_future 持有文件读取的状态 Note over Future: .await 完成后切换到状态 2 Future-Future: content 读取结果 Future-Future: write_future 异步写操作 Note over Future: write_future 内部引用了 content Note over Future: ⚠️ 自引用结构形成 Code-Future: 如果此时 Future 被移动... Future--Code: content 地址变化write_future 悬垂四、Pin 的解决方案钉在原地Pin的作用就一句话承诺被 Pin 住的值不会再被移动。use std::pin::Pin; // PinP 包装了一个指针类型 P // 只要值被 Pin 住了就不能通过安全代码获取 mut T 来移动它 // 最常用PinBoxT —— 堆上分配 钉住 let pinned_future: PinBoxdyn FutureOutput String Box::pin(read_and_process(input.txt));Pin的设计关键在于Unpintraitflowchart TD Start[所有类型] -- Question{实现了 Unpin} Question --|是大多数类型| UnpinSafe[Pin 不限制移动\n可以自由地获取 mut T\n不会产生安全问题] Question --|否自引用类型| NotUnpin[Pin 限制移动\n不能获取 mut T\n必须通过 Pin 安全访问] NotUnpin -- Example[例子\n- 编译器生成的 Future\n- 自引用结构体\n- async 块] UnpinSafe -- Example2[例子\n- i32, String, Vec\n- 普通的 struct/enum\n- Box, Arc, Rc] style NotUnpin fill:#ffcdd2 style UnpinSafe fill:#c8e6c9!Unpin没有实现 Unpin的类型被 Pin 住后安全代码无法获取mut T所以无法移动它。这就是 Pin 的安全保证。下面是一个手动实现的自引用结构展示 Pin 的正确用法use std::pin::Pin; use std::marker::PhantomPinned; /// 一个自引用结构内部保存字符串和指向它的切片引用 /// !Unpin 意味着一旦被 Pin 住就不能移动 struct SelfRef { data: String, // 实际数据 // 这个指针字段指向 data // 注意我们不能用普通的 str因为生命周期不够长 // 这里用 NonNullu8 len 来假装是一个 str ptr: *const u8, // 指向 data 内部某处的裸指针 len: usize, // 引用的长度 _pin: PhantomPinned, // 标记为 !Unpin // PhantomPinned 是一个零大小的标记类型 // 它让 SelfRef 不再是 Unpin } impl SelfRef { /// 创建一个新的自引用结构未初始化 ptr fn new(data: String) - Self { Self { data, ptr: std::ptr::null(), // 先用空指针占位 len: 0, _pin: PhantomPinned, } } /// 初始化自引用让 ptr 指向 data 的某个位置 /// 必须在 Pin 住之后才能调用 /// /// # Safety /// /// 调用者必须保证 /// 1. self 已经被 Pin 住不会被移动 /// 2. data 字段不会被修改避免 reallocation unsafe fn init(self: Pinmut Self) { // 通过 Pin 获取内部的不可变引用是安全的 let this self.get_unchecked_mut(); // 这里使用 unsafe因为我们在创建一个自引用 this.ptr this.data.as_ptr(); this.len this.data.len(); } /// 获取自引用的内容读取操作是安全的因为不会移动 fn get_ref(self: PinSelf) - str { // 从一个 Pin 住的 SelfRef 读取数据是安全的 unsafe { let this self.get_ref(); // 根据裸指针和长度构造一个 str std::str::from_utf8( std::slice::from_raw_parts(this.ptr, this.len) ).unwrap() } } } // 使用示例 fn example() { // 第一步创建结构体用 Box::pin 把它钉在堆上 let mut self_ref Box::pin(SelfRef::new(hello world.to_string())); // 第二步初始化自引用安全因为已经被 Pin 住了 // 注意init 是 unsafe因为它在创建自引用 // 实际项目中你需要在 unsafe { } 块里调用 unsafe { self_ref.as_mut().init(); } // 第三步读取自引用 let result self_ref.as_ref().get_ref(); println!(自引用的内容: {}, result); // 输出: hello world // ❌ 不能这样做PinBoxT 不能解 Pin 然后 move // let moved *self_ref; // 编译错误 }几个关键概念解释为什么需要unsafe来 init因为 Rust 的类型系统无法在编译时证明我正在创建一个自引用但这个值确实不会移动。所以在创建自引用的那一刻必须用 unsafe 向编译器承诺我知道我在做什么我保证这个值不会被移动。为什么读取不需要 unsafe因为读取 (get_ref) 是纯读操作。即使值没有 Pin 住读取也不会破坏安全性最差就是读到悬垂指针那也是创建者的责任不是读取者的责任。但 Pin 的设计保证了如果创建者是安全的读取者也是安全的。PhantomPinned 是什么它是一个零大小的标记类型实现了!Unpin。把PhantomPinned加到结构体里编译器就知道这个类型不能安全移动。这就像在结构体上贴了一张此物品不能被移动的标签。作为普通开发者你可能不需要手动实现自引用结构。但有三件事要心里有数1.async fn和async {}块返回的 Future 都是!Unpin的这意味着你不能随意移动它们。用tokio::spawn时为什么要async { ... }而不是直接传 Future// ❌ 直接 spawn 一个包含引用的 Future 有时候会编译错误 // 因为 Future 不是 static Send且移动可能破坏自引用 // tokio::spawn(my_async_fn(local_var)); // ✅ 把局部变量 move 进去Future 变成自包含的 let local_var String::from(hello); tokio::spawn(async move { // 现在 local_var 的所有权在 Future 内部了 use_value(local_var).await; });其实tokio::spawn要求Future: Send static而PinBoxdyn Future恰好满足。Tokio 内部会帮你 Pin 住它。2.futures::select!和tokio::select!需要变量被 Pin 住use tokio::time::{sleep, Duration}; async fn task_one() - static str { sleep(Duration::from_secs(2)).await; 任务一完成 } async fn task_two() - static str { sleep(Duration::from_secs(1)).await; 任务二完成我更快 } #[tokio::main] async fn main() { // ✅ 用 pin! 宏把 Future 钉在栈上 let f1 task_one(); tokio::pin!(f1); // 钉住 f1现在它可以安全地在 select! 里使用了 let f2 task_two(); tokio::pin!(f2); tokio::select! { result mut f1 { println!(f1 先完成: {}, result); } result mut f2 { println!(f2 先完成: {}, result); } } }select!宏内部需要轮询多个 Future。如果一个 Future 先完成另一个还在等那么另一个的状态必须保持有效。如果 Future 在此期间被移动了状态就丢了。所以必须 Pin 住。3. 需要!Unpin类型的特征对象时用PinBoxdyn Futureuse std::future::Future; use std::pin::Pin; /// 一个通用的 Future 工厂 fn make_future(flag: bool) - PinBoxdyn FutureOutput String { if flag { // async 块返回的 Future 是 !Unpin 的 Box::pin(async { 分支 A.to_string() }) } else { Box::pin(async { 分支 B.to_string() }) } }五、总结回顾我对 Pin 的理解过程有几个关键转折点不要从 Pin 本身开始学要从Rust 为什么需要 Pin开始。Pin 是为了解决自引用结构的移动安全问题。Future 是自引用结构是因为编译器的状态机生成。跨.await的局部变量和引用被编码在同一个枚举变体里移动就悬垂。PinP不是魔法它只是限制了对P的访问不让获取mut T从而阻止了安全代码中的移动操作。Unpin和!Unpin是关键。大多数类型是Unpin的Pin 不限制它们。编译器生成的 Future 是!Unpin的Pin 限制它们。日常开发不需要手动实现 Pin但理解它能帮你读懂编译错误知道为什么select!需要pin!为什么 spawn 某些 Future 会编译失败。如果你和我一样是自学 Rust 的新手建议不要死磕 Pin 的源码。先理解为什么要 Pin再在用到的时候查文档慢慢就会了。