Rust 里最让人头疼的两个类型:Pin 和 Unpin,究竟解决了什么问题?
原文链接:https://blog.cloudflare.com/pin-and-unpin-in-rust/ 假设你想写一个工具类型,能把任意异步函数包一层,额外记录它的执行耗时: 接口设计挺优雅的。下面来实现它: 实现 编译器直接报错: 这个错误对初学者来说相当令人困惑——明明 搞清楚这两个问题,是理解 Rust 异步编程的一道必经之坎。 在 Rust 里, 调用 一个最简单的 Future 实现: 注意 要理解 自引用类型,就是结构体内部的某个字段指向自身另一个字段的地址。设想有这样一个结构体: 初始状态下一切正常, 但是,如果这个结构体被移动了,会发生什么? 在 Rust 中,"移动"意味着把数据从一个内存位置搬到另一个位置。常见触发场景:把结构体传入函数、放进 移动之后, 结果:悬垂指针。轻则程序崩溃,重则产生可被利用的安全漏洞。 为什么 当你写下一个 这就是为什么 在深入讲 Rust 把所有类型分成两类: 第一类:可以安全移动的类型。 绝大多数类型都属于这一类,比如数字、字符串、布尔值,以及由这些类型组成的结构体和枚举。它们没有自引用,移动之后所有字段的值依然有效。这些类型自动实现 第二类:不能安全移动的类型。 也就是自引用类型,它们在 trait 系统里被标记为 如果类型是 简单来说: 这就是为什么 现在回头看 从一个被 Pin 住的结构体中,访问其各个字段的过程,叫做 projection(投影)。规则是这样的: 手动实现 projection 需要 加上 现在可以正确实现 全程没有 梳理一下这篇文章讲的内容: 为什么需要自引用类型? 因为 Rust 的 自引用类型为什么不能移动? 因为移动只搬数据,不更新内部的指针,移动之后指针就悬空了,产生未定义行为。 Unpin 是什么? 一个标记 trait,实现了它的类型表示"随便移动,安全无虞"。绝大多数类型自动实现了 Pin 是什么? 一个包装类型,用于包裹指针,承诺所指向的值在 Pin 存活期间不会被移动。对 为什么 Future::poll 要求 怎么在实践中使用? 用 作为普通的 async Rust 用户,日常写代码几乎不会直接碰到 理解了"为什么","怎么做"就容易多了。本文基于 Cloudflare 工程师 Adam Chalmers 的技术博客,从一个实际编程场景出发,由浅入深地讲清楚 Rust 中 Pin、Unpin 与自引用类型的来龙去脉。
一个看起来很简单的需求
let async_fn = reqwest::get("http://example.com");
let timed = TimedWrapper::new(async_fn);
let (resp, duration) = timed.await;
println!("耗时 {}ms,状态码 {}", duration.as_millis(), resp.unwrap().status());pub struct TimedWrapper<Fut: Future> {
start: Option<Instant>,
future: Fut,
}Future trait,在 poll 方法里调用内层 Future:fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let start = self.start.get_or_insert_with(Instant::now);
let inner_poll = self.future.poll(cx); // 编译报错!
// ...
}Fut 类型没有 poll 方法,只有 Pin<&mut Fut> 才有。Fut 实现了 Future,为什么不能直接调用 poll?Pin 是什么?为什么它要出现在这里?先从 Future 说起
async fn 本质上是一个返回 Future 的普通函数。Future trait 只有一个方法:poll。poll,它会返回两种结果:Poll::Pending(还没好,待会再来)或者 Poll::Ready(value)(完成了,结果在这里)。异步运行时(比如 Tokio)就是在不断地轮询各个 Future,谁 Ready 了就把结果给出去。struct RandFuture;
impl Future for RandFuture {
type Output = u16;
fn poll(self: Pin<&mut Self>, _cx: &mut Context) -> 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 所在的内存地址,读取它能得到合法的值。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。Unpin trait(它是一个自动 trait,类似 Send 和 Sync,不需要手动实现)。!Unpin(! 表示"不实现")。数量很少,但一旦被错误移动就会产生未定义行为。Unpin 这个名字乍看有点反直觉——它不是说"这个类型可以被 unpin",而是说"这个类型根本不需要被 pin,随便移动都安全"。Pin:给不能移动的类型上一把锁
Pin<P> 是一个包装类型,它包裹一个指针 P,并做出如下保证:如果 P 指向的类型是 !Unpin,那么这个值在 Pin 存活期间不会被移动。Unpin,Pin 就没什么限制效果,你随时可以取出值来移动。如果类型是 !Unpin,Pin 就是一把锁,取值只能通过 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>。Unpin,可以直接拿到 &mut T(普通引用),随便用;!Unpin(比如内嵌的 Future),要拿到 Pin<&mut T>,否则会破坏 Pin 的保证。unsafe 代码,且容易出错。好在有一个 crate 帮你做这件事。pin-project:让 projection 变得安全又简洁
pin-project 这个 crate 通过过程宏自动生成安全的 projection 代码。用法很直观:#[pin_project::pin_project]
pub struct TimedWrapper<Fut: Future> {
start: Option<Instant>,
#[pin] // 标记这个字段需要 Pin projection
future: Fut,
}#[pin_project] 之后,它会自动生成一个 project() 方法。对标记了 #[pin] 的字段,project() 返回 Pin<&mut Fut>;对其余字段,返回普通的 &mut T。poll 了:fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let mut this = self.project(); // 调用自动生成的 projection
let start = this.start.get_or_insert_with(Instant::now);
let inner_poll = this.future.as_mut().poll(cx); // 正确!
let elapsed = start.elapsed();
match inner_poll {
Poll::Pending => Poll::Pending,
Poll::Ready(output) => Poll::Ready((output, elapsed)),
}
}unsafe,编译通过,功能正确。把整个脉络串起来
async/await 编译出来的状态机,天然需要在不同 await 点之间保存引用,由此产生自引用结构。Unpin。!Unpin 类型(比如 async 状态机),这个承诺由类型系统强制执行。Pin<&mut Self>? 因为很多 Future 内部是自引用的,在 poll 时不能被移动,所以调用方必须通过 Pin 做出这个承诺。pin-project crate,加几个属性宏,让编译器帮你生成安全的 projection 代码,不用手写 unsafe。写在最后
Pin 和 Unpin 是 Rust 类型系统中为数不多的"晦涩角落"之一,但它们的存在是有充分理由的。没有它们,Rust 的 async/await 就无法在不引入 GC 的前提下保证内存安全。Pin——运行时和库都帮你处理了。但一旦你开始写自己的异步基础设施、封装 Future、或者实现某些底层 trait,这套机制就变成了绕不过去的知识。