news 2026/8/31 7:38:09

手写MiniPin:从自引用到async彻底理解Rust Pin

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写MiniPin:从自引用到async彻底理解Rust Pin

很多 Rust 开发者第一次被Pin卡住,不是在看文档的时候,而是在写异步代码的时候。一个看起来完全合理的async fn,编译时突然抛出一大屏错误,里面反复出现future is not Sendcannot be sent between threads safely。网上一搜,有人说“用Box::pin包一下”,你照做了,确实能编过,但完全不清楚Pin到底做了什么。

这篇文章想换一种讲法:不直接背Pin的 API,而是从第一性原理出发,自己动手实现一个“近似版 Pin”。我会带着你从“自引用结构体为什么危险”这个问题开始,一步步推导出Pin的核心设计。你会发现,Pin并不是 Rust 为了async临时打上的补丁,而是“移动语义 + 自引用安全”这对矛盾下自然生长出来的基础设施。

先给一个明确判断:Pin真正难的地方不是unsafe,而是大多数学习者缺少一条完整的 “为什么需要它” 的问题链。等你自己实现完一个MiniPin,再回头看标准库的Pin,很多曾经觉得玄妙的细节会自然串起来。

1. 为什么 Rust 需要一个叫 Pin 的东西

1.1 移动语义:Rust 里最便宜也最危险的操作

Rust 的赋值、传参、函数返回,默认都是移动语义。移动的本质是一次位拷贝,同时原位置“逻辑上失效”。对绝大多数类型来说,移动是安全且高效的,编译器可以随便把StringVec从一个栈帧搬到另一个栈帧,所有堆内存的指针都会跟着搬走,因为指针本身就是被移动结构体的一部分。

问题出在自引用结构体。如果一个结构体内部有一个指针或引用,指向的是它自己的某个字段,那么移动整个结构体时,这个内部指针不会被更新,它仍然指向旧的内存地址。旧地址在语义上已经不属于这个对象,继续通过它访问数据就是未定义行为。

1.2 用一段安全代码观察“移动导致地址失效”

这里先用一个安全的示例说明地址变化,不涉及任何未定义行为:

// 文件路径:examples/self_addr_demo.rs // 演示:移动一个结构体时,内部记录的旧地址并不会被更新。 #[derive(Debug)] struct SelfReferential { data: [u8; 32], // 记录 data[0] 的地址,模拟一个“指向自身字段的指针” data_ptr_addr: usize, } impl SelfReferential { fn new() -> Self { SelfReferential { data: [0u8; 32], data_ptr_addr: 0, } } fn init_ptr_addr(&mut self) { self.data_ptr_addr = &self.data[0] as *const u8 as usize; } } fn main() { let mut a = SelfReferential::new(); a.init_ptr_addr(); println!("a.data[0] 实际地址: {:#x}", &a.data[0] as *const u8 as usize); println!("a.data_ptr_addr 记录: {:#x}", a.data_ptr_addr); // 移动 a 到 b let b = a; println!("--- 移动之后 ---"); println!("b.data[0] 实际地址: {:#x}", &b.data[0] as *const u8 as usize); println!("b.data_ptr_addr 记录: {:#x}", b.data_ptr_addr); }

这段代码里没有使用任何unsafe。移动前后,b.data[0]的实际地址和结构体内部记录的地址不再一致。如果内部真的有一个指针依赖这个地址来访问数据,移动之后就会读到错误的位置。

真实项目中更常见的情况是:结构体持有*const Tusize偏移量、或者一个生命周期关联的&self引用。这些东西都依赖“对象地址不再变化”这一前提。

1.3 小结:Pin 要解决的问题

Pin的使命不是提高性能,也不是让对象“不可变”,而是解决一个非常具体的问题:当一个值必须保持地址稳定时,阻止代码通过安全方式移动它。

2. Pin 与 async Future:自引用在现实中最常见的入口

理解Pin的经典场景是异步编程。为什么async fn编译出来的 future 需要Pin?因为 future 本质上是一个状态机,编译器会把每个await点之间用到的局部变量保存到这个状态机里。

看这段代码:

async fn example() ->
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 7:35:57

iOS校招笔试高频考点拆解:从内存管理到GCD底层原理

有段时间没聊校招的事了,刚好有位读者发来一份《哔哩哔哩2020校园招聘 iOS 方向笔试卷(二)》的回忆版,让我帮忙把里面的知识点拆一拆。这份卷子虽然是2020年的,但看完之后我的第一反应是:它里面考的底层原理…

作者头像 李华
网站建设 2026/8/31 7:34:20

后台开发校招笔试备考全攻略:从乐信真题看考点与策略

又是一年校招季,后台开发的笔试题永远是大家最焦虑的一关。我当年参加乐信2019校园招聘后台开发工程师笔试题时,最大的感受是:题目本身不算偏门,但覆盖面极广,从数据结构、操作系统到网络协议、Linux命令,再…

作者头像 李华
网站建设 2026/8/31 7:34:14

STM32N6链接报错undefined reference?一文教你排查MX_USART1_UART_Init缺失问题

最近在折腾 STM32N6,这颗芯片和之前玩过的 M 系列有个很不一样的地方:内部直接集成了 Neural-ART NPU,跑 AI 模型不再依赖 CPU 纯算硬扛。我照着官方 Neural-ART 教程拉了一个示例工程,目标是在一个轻量分类模型上把推理结果通过串…

作者头像 李华
网站建设 2026/8/31 7:33:48

快手2020秋招算法岗B卷:KMP、动态规划与机器学习考点全解析

站在求职者的角度看,快手2020年秋招算法岗的这套B卷,其实挺有代表性的。它不像一些公司那样偏爱刁钻的数学证明,也不像另一些公司那样几乎全是深度学习模型八股,它的题目构成更像是一场“算法基本功工程思维”的综合性体检。我当年…

作者头像 李华
网站建设 2026/8/31 7:31:24

DeepSeek接入Codex:配置Skill与插件打造Agent编程工作流

如果你最近在关注 AI 编程助手,一定刷到过类似“DeepSeek 接入 Codex”的讨论。标题里的“王炸”也许有点夸张,但这个组合确实解决了一个非常现实的问题:模型能力不再是唯一瓶颈,Agent 的工程化能力开始变成开发者自己能掌控的东西…

作者头像 李华
网站建设 2026/8/31 7:30:52

Spring 声明式事务在同类中失效的原因与解决方案汇总

Spring 声明式事务在同类中失效的原因与解决方案汇总 在使用 Spring 声明式事务(Transactional)时,一个非常常见又容易踩坑的场景是:同一个类中,一个方法内部调用另一个带有 Transactional 的方法,结果事务…

作者头像 李华