当前位置: 首页 > news >正文

Rust 里最让人头疼的两个类型:Pin 和 Unpin,究竟解决了什么问题?

本文基于 Cloudflare 工程师 Adam Chalmers 的技术博客,从一个实际编程场景出发,由浅入深地讲清楚 Rust 中 Pin、Unpin 与自引用类型的来龙去脉。

原文链接:https://blog.cloudflare.com/pin-and-unpin-in-rust/


一个看起来很简单的需求

假设你想写一个工具类型,能把任意异步函数包一层,额外记录它的执行耗时:

letasync_fn=reqwest::get("http://example.com");lettimed=TimedWrapper::new(async_fn);let(resp,duration)=timed.await;println!("耗时 {}ms,状态码 {}",duration.as_millis(),resp.unwrap().status());

接口设计挺优雅的。下面来实现它:

pubstructTimedWrapper<Fut:Future>{start:Option<Instant>,future:Fut,}

实现Futuretrait,在poll方法里调用内层 Future:

fnpoll(self:Pin<&mutSelf>,cx:&mutContext)->Poll<Self::Output>{letstart=self.start.get_or_insert_with(Instant::now);letinner_poll=self.future.poll(cx);// 编译报错!// ...}

编译器直接报错:Fut类型没有poll方法,只有Pin<&mut Fut>才有。

这个错误对初学者来说相当令人困惑——明明Fut实现了Future,为什么不能直接调用pollPin是什么?为什么它要出现在这里?

搞清楚这两个问题,是理解 Rust 异步编程的一道必经之坎。


先从 Future 说起

在 Rust 里,async fn本质上是一个返回Future的普通函数。Futuretrait 只有一个方法:poll

调用poll,它会返回两种结果:Poll::Pending(还没好,待会再来)或者Poll::Ready(value)(完成了,结果在这里)。异步运行时(比如 Tokio)就是在不断地轮询各个 Future,谁 Ready 了就把结果给出去。

一个最简单的 Future 实现:

structRandFuture;implFutureforRandFuture{typeOutput=u16;fnpoll(self:Pin<&mutSelf>,_cx:&mutContext)->Poll<Self::Output>{Poll::Ready(rand::random())}}

注意poll方法的接收者不是&mut self,而是Pin<&mut Self>。这个细节正是问题的核心所在。


自引用类型:一个危险的结构

要理解Pin,必须先理解它要解决的问题:自引用类型(self-referential types)

自引用类型,就是结构体内部的某个字段指向自身另一个字段的地址。设想有这样一个结构体:

struct MyStruct { val: i32, // 存储在内存地址 A pointer: *const i32, // 指向地址 A }

初始状态下一切正常,pointer指向val所在的内存地址,读取它能得到合法的值。

但是,如果这个结构体被移动了,会发生什么?

在 Rust 中,"移动"意味着把数据从一个内存位置搬到另一个位置。常见触发场景:把结构体传入函数、放进Box、或者Vec扩容重新分配内存……

移动之后,val搬到了新地址 B,但pointer字段里存的值还是老地址 A,而 A 处的内存已经不再属于这个结构体,随时可能被别的数据覆盖。

结果:悬垂指针。轻则程序崩溃,重则产生可被利用的安全漏洞。


偏偏 Future 经常是自引用的

为什么Future和自引用有关?

当你写下一个async函数,Rust 编译器会把它编译成一个状态机。这个状态机需要在各个await点之间保存局部变量。如果某个变量在await前被借用,借用的引用也需要被保存在状态机里——而这个状态机本身就包含了这个借用,于是形成了自引用。

这就是为什么Future::poll的接收者不能是普通的&mut self,而必须是Pin<&mut Self>——这是在要求调用方保证:调用 poll 之前,这个 Future 已经被"钉住",不会再被移动。


Unpin:大多数类型的默认状态

在深入讲Pin之前,先讲Unpin

Rust 把所有类型分成两类:

第一类:可以安全移动的类型。绝大多数类型都属于这一类,比如数字、字符串、布尔值,以及由这些类型组成的结构体和枚举。它们没有自引用,移动之后所有字段的值依然有效。这些类型自动实现Unpintrait(它是一个自动 trait,类似SendSync,不需要手动实现)。

第二类:不能安全移动的类型。也就是自引用类型,它们在 trait 系统里被标记为!Unpin!表示"不实现")。数量很少,但一旦被错误移动就会产生未定义行为。

Unpin这个名字乍看有点反直觉——它不是说"这个类型可以被 unpin",而是说"这个类型根本不需要被 pin,随便移动都安全"。


Pin:给不能移动的类型上一把锁

Pin<P>是一个包装类型,它包裹一个指针P,并做出如下保证:如果 P 指向的类型是!Unpin,那么这个值在Pin存活期间不会被移动。

如果类型是UnpinPin就没什么限制效果,你随时可以取出值来移动。如果类型是!UnpinPin就是一把锁,取值只能通过 unsafe 代码,编译器用这种方式迫使你明确表态:“我知道这里有风险,我已经仔细审查过。”

简单来说:Pin不是锁住指针本身,而是锁住指针所指向的,让它不能被移动。

Pin<&mut T>:给我一个对 T 的可变引用,但我保证在此期间不会移动 T。

这就是为什么Future::poll要求Pin<&mut Self>:执行器(executor)通过Pin向 Future 承诺,在调用poll的过程中,不会把这个 Future 移动到别的地方去。


回到原来的问题:怎么调用内层 Future 的 poll?

现在回头看TimedWrapper的问题。我们有一个Pin<&mut TimedWrapper<Fut>>,想要调用self.future.poll(cx),也就是需要一个Pin<&mut Fut>

从一个被 Pin 住的结构体中,访问其各个字段的过程,叫做projection(投影)。规则是这样的:

  • 如果字段类型是Unpin,可以直接拿到&mut T(普通引用),随便用;
  • 如果字段类型是!Unpin(比如内嵌的 Future),要拿到Pin<&mut T>,否则会破坏 Pin 的保证。

手动实现 projection 需要unsafe代码,且容易出错。好在有一个 crate 帮你做这件事。


pin-project:让 projection 变得安全又简洁

pin-project这个 crate 通过过程宏自动生成安全的 projection 代码。用法很直观:

#[pin_project::pin_project]pubstructTimedWrapper<Fut:Future>{start:Option<Instant>,#[pin]// 标记这个字段需要 Pin projectionfuture:Fut,}

加上#[pin_project]之后,它会自动生成一个project()方法。对标记了#[pin]的字段,project()返回Pin<&mut Fut>;对其余字段,返回普通的&mut T

现在可以正确实现poll了:

fnpoll(self:Pin<&mutSelf>,cx:&mutContext)->Poll<Self::Output>{letmutthis=self.project();// 调用自动生成的 projectionletstart=this.start.get_or_insert_with(Instant::now);letinner_poll=this.future.as_mut().poll(cx);// 正确!letelapsed=start.elapsed();matchinner_poll{Poll::Pending=>Poll::Pending,Poll::Ready(output)=>Poll::Ready((output,elapsed)),}}

全程没有unsafe,编译通过,功能正确。


把整个脉络串起来

梳理一下这篇文章讲的内容:

为什么需要自引用类型?因为 Rust 的async/await编译出来的状态机,天然需要在不同await点之间保存引用,由此产生自引用结构。

自引用类型为什么不能移动?因为移动只搬数据,不更新内部的指针,移动之后指针就悬空了,产生未定义行为。

Unpin 是什么?一个标记 trait,实现了它的类型表示"随便移动,安全无虞"。绝大多数类型自动实现了Unpin

Pin 是什么?一个包装类型,用于包裹指针,承诺所指向的值在 Pin 存活期间不会被移动。对!Unpin类型(比如 async 状态机),这个承诺由类型系统强制执行。

为什么 Future::poll 要求Pin<&mut Self>因为很多 Future 内部是自引用的,在 poll 时不能被移动,所以调用方必须通过 Pin 做出这个承诺。

怎么在实践中使用?pin-projectcrate,加几个属性宏,让编译器帮你生成安全的 projection 代码,不用手写 unsafe。


写在最后

PinUnpin是 Rust 类型系统中为数不多的"晦涩角落"之一,但它们的存在是有充分理由的。没有它们,Rust 的async/await就无法在不引入 GC 的前提下保证内存安全。

作为普通的 async Rust 用户,日常写代码几乎不会直接碰到Pin——运行时和库都帮你处理了。但一旦你开始写自己的异步基础设施、封装 Future、或者实现某些底层 trait,这套机制就变成了绕不过去的知识。

理解了"为什么","怎么做"就容易多了。


http://www.jsqmd.com/news/700449/

相关文章:

  • ml-intern数据挖掘功能:从大数据中发现知识
  • 2026 深圳 GEO 服务商优选榜单:五家头部机构综合实力与口碑测评 - GEO优化
  • 2026 上海 GEO 服务商甄选指南:五家标杆企业综合测评与行业口碑盘点 - GEO优化
  • 如何用Pentaho Kettle构建现代企业数据管道:从异构数据源到统一数据湖
  • Ubuntu 终端不能补全
  • 终极微信自动化指南:5分钟快速构建企业级微信机器人
  • Furion日志系统完全配置指南:从控制台输出到分布式日志收集
  • 2026 北京 GEO 优质服务商深度测评:五家头部机构实力与口碑综合榜单 - GEO优化
  • Microsandbox:为AI Agent打造毫秒级启动的硬件隔离沙盒
  • 机器学习数据预处理:数据编码
  • 终极MDCX Docker容器化部署指南:从架构解析到高效运维
  • Duolingo免费开放九种语言高级学习内容
  • Algorithm-Implementations 部署与扩展:Web应用与Android应用完整开发指南
  • 如何快速上手Pointer-Generator:10分钟构建你的第一个摘要模型
  • SMS Backup+:安卓短信备份终极指南,一键安全保护你的通信记录
  • GetSSL高级配置指南:双RSA/ECDSA证书和多服务器部署
  • gh_mirrors/lib/libnetwork:终极容器网络解决方案完全指南
  • Elementary测试框架详解:构建可靠的数据质量监控
  • 第69篇:从Transformer到扩散模型——主流AI生成模型的核心思想演进(原理解析)
  • VSCode + Prettier 配置全攻略:让你的微信小程序开发体验提升一个档次
  • 用自家产品构建自家产品:Cloudflare Images 的工程架构解析
  • 如何快速上手ModernGL:10个简单步骤掌握Python 3D图形编程
  • SQL报表聚合中间结果过大_分阶段统计
  • 10个Electron Release Server最佳实践:提升应用发布效率
  • ml-intern更新日志:了解最新功能与改进
  • 终极Windows系统优化指南:5分钟掌握WinUtil一键管理工具
  • Go-arg高级用法:子命令、环境变量和自定义验证的完整教程
  • FigmaCN:3分钟搞定Figma中文界面的终极指南
  • GPT-5.5降临:OpenAI打造最强智能体,引领AI工作新纪元!
  • PHP Server Monitor高级监控技巧:服务端口与网站URL监控实战