Rust 中的 `std::pin::Pin` 是什么?
摘要
解释 Rust 中的 `std::pin::Pin` 类型,为什么自引用类型(如异步状态机)需要固定(pinning),以及 `Pin` 如何防止移动同时允许安全修改。
<p><a href="https://lobste.rs/s/ltzfkv/what_is_std_pin_pin_rust">评论</a></p>
查看缓存全文
缓存时间: 2026/06/29 18:30
# Rust 中的 `std::pin::Pin` 是什么? 来源:https://vrong.me/blog/what-is-pinning-in-rust/ `std::pin::Pin` 是一个指针包装器,它表示一个保证:通过该指针指向的对象不会被移动。 ## 为什么需要它? 需要固定(pinning)的根本原因来自自引用类型(self-referential types)。自引用结构体是地址敏感类型(address-sensitive types)的最常见例子,也是 `Pin` 的主要动机。以下面的结构体为例: ```rust
1struct SelfRef {
2 data: i32,
3 ptr: *const i32, // 指向 self.data
4}
``` 如果我们移动这个结构体的实例(例如,通过将所有权转移到另一个变量或返回它),它的内存地址会改变。然而,裸指针 `ptr` 仍然指向旧的内存位置,从而产生悬垂指针。因此,我们需要一种方法,在建立这些自引用之后,防止移动 `SelfRef`。 ### 我们在哪里遇到这个问题? 我们在使用 `async`/`await` 和 `Futures` 时最常遇到这个问题。跨 `.await` 点生存的局部变量会成为编译器生成的状态机中的字段。如果对一个局部变量的引用也跨过同一个 `.await` 点生存,那么生成的 future 就会变成自引用的。一旦开始轮询(polling),future 可能依赖指向自身其他字段的内部引用。之后移动 future 会使这些引用失效。为了防止这一点,`Future::poll` 方法要求 future 被固定: ```rust
1pub trait Future {
2 type Output;
3 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll;
4}
``` 通过接受 `Pin<&mut Self>` 而不是 `&mut self`,`poll` 的调用者被要求保证 future 在开始轮询后不会被移动。 ## 固定是如何工作的? `Pin` 阻止安全代码通过该指针移动目标,同时仍然允许对固定值进行普通的修改。 ### `&mut T` 的问题 如果你有一个可变引用 `&mut T`,像 `mem::replace`、`mem::swap` 或赋值这样的函数可以重新定位存储在该内存位置的值。`Pin` 限制了你恢复普通可变引用的能力。安全代码无法从 `Pin<&mut T>` 恢复普通的 `&mut T`,除非 `T: Unpin`。 ```rust
1impl<'a, T: ?Sized> Pin<&'a T> {
2 pub const fn get_ref(self) -> &'a T { ... }
3}
4
5impl<'a, T: ?Sized> Pin<&'a mut T> {
6 pub const fn get_mut(self) -> &'a mut T
7 where
8 T: Unpin
9 { ... }
10}
``` 重要:`Pin` 只阻止通过固定指针的移动;它不阻止对固定值的修改。固定类型的方法可以自由地修改其字段,只要它们不移动字段即可。如果类型没有实现 `Unpin`(即它是 `!Unpin`),你就无法通过安全代码获得 `&mut T`。你必须使用像 `Pin::get_unchecked_mut` 这样的 `unsafe` 方法,向编译器表明你承诺不会通过该引用将值移出。 ### 什么是 `Unpin`? 实现了 `Unpin` 的类型不依赖固定来保证正确性。 ```rust
// std::marker
pub auto trait Unpin {}
``` Rust 中的大多数类型(如 `i32`、`String`、`Vec` 等)不关心是否被移动,因此默认是 `Unpin`。除非显式实现了 `!Unpin`,否则所有类型自动实现 `Unpin`。 提示:标记结构体 `std::marker::PhantomPinned` 显式地是 `!Unpin`。由于自动 trait 会自动传播,任何包含 `PhantomPinned` 字段的结构体也会自动成为 `!Unpin`。 ```rust
1use std::marker::PhantomPinned;
2
3struct SelfRef {
4 data: i32,
5 ptr: *const i32,
6 _phantom: PhantomPinned, // 使整个结构体成为 !Unpin
7}
``` 这是声明自定义结构体在固定后移动不安全的标准方式。由于编译器无法自动检测自引用(通常通过不安全的裸指针创建),它无法自动将这样的结构体标记为 `!Unpin`。因此,这依赖于一个约定:开发者必须为任何自引用结构体显式选择退出 `Unpin`(通常通过嵌入一个 `PhantomPinned` 字段)。如果自引用类型错误地保持 `Unpin`,安全代码可以从 `Pin` 中恢复可变引用并移动该值,从而违反创建自引用的不安全代码所做的假设。 `Pin` 不会物理上阻止值的移动。相反,它是一种类型层面的保证,即该值不会通过该指针被移动。因此,安全地构造一个 `Pin` 需要确保目标在该 pin 的生命周期内保持在一个稳定的内存位置上。 ## 如何构造一个 `Pin` `Pin` 本身并不会固定一个值。相反,构造一个 `Pin` 意味着证明目标在该 pin 的生命周期内将保持在一个稳定的内存位置上。 ### `Pin::new` 构造 Pin 的最简单方法是使用 `Pin::new`: ```rust
1let mut value = 42;
2let pinned = Pin::new(&mut value);
``` 然而,这个构造函数只有在 `T: Unpin` 时才可用。由于 `Unpin` 类型不依赖固定来保证正确性,将它们包装在 `Pin` 中总是安全的。固定保证实际上是一个空操作。 ### `pin!` 当你需要在不进行堆分配的情况下在本地固定一个值时,可以使用 `pin!` 宏: ```rust
1use std::pin::pin;
2
3let future = pin!(async {
4 println!("Hello");
5});
``` 该宏创建一个局部变量,并返回一个指向它的 `Pin<&mut T>`。编译器保证该局部变量在其生命周期剩余部分不会被移动,这使得它成为在栈上安全固定 `!Unpin` 值的方法。 警告:尽管名字叫 `pin!`,但它并没有固定栈内存本身。它创建了一个固定引用,其生命周期与局部变量绑定。一旦变量离开作用域,固定保证就结束了。 ### `Box::pin` 对于 `!Unpin` 类型,最常用的构造函数是 `Box::pin`: ```rust
1let pinned = Box::pin(SelfRef { ... });
``` 与创建绑定到局部变量的 `Pin<&mut T>` 的 `pin!` 不同,`Box::pin` 返回一个 `Pin<Box<T>>`,其生命周期由 `Box` 拥有。由于堆分配本身不会移动,目标在 `Box` 的生命周期内拥有一个稳定的内存位置,因此可以安全地固定。 注意:移动 `Box` 本身并不会移动它拥有的值。只有存储在 `Box` 内部的指针会被移动;堆分配仍然位于同一个地址。 ### `Pin::new_unchecked` 有时安全构造函数无法证明一个值将保持在原位。在这些情况下,不安全的代码可以手动构造一个 `Pin`: ```rust
1let pinned = unsafe { Pin::new_unchecked(ptr) };
``` 通过调用 `Pin::new_unchecked`,调用者承诺目标不会再通过任何指针被移动,且这个承诺在生成的 `Pin` 的生命周期内有效。如果这个承诺被违反,任何依赖固定保证的代码都可能表现出未定义行为。因此,`Pin::new_unchecked` 通常只在实现能够维持这个不变量的底层抽象时使用。 ## 你实际上什么时候需要关心? 对于大多数 Rust 开发者来说,`Pin` 和 `Unpin` 在后台安静地工作。你通常只需要在两种场景下考虑它们: 1. **使用异步代码:** 如果你需要手动轮询一个 future,或者将一个 future 传递给需要固定 future 的 API,可以使用 `Box::pin(future)`(在堆上固定它)或 `std::pin::pin!(future)`(在栈上本地固定它)。 2. **手动实现 `Future`:** 如果你正在编写一个自定义状态机或其他底层异步原语,你将需要处理 `Pin<&mut Self>`,并可能需要使用 `PhantomPinned` 和不安全代码来维护固定不变式。 最终,`Pin` 是 Rust 针对地址敏感类型问题的零成本解决方案。它使得方便的 `async`/`await` 和其他自引用抽象成为可能,同时保留了 Rust 的内存安全保证,且不需要垃圾回收器。
相似文章
没人要求的 `Sync` 约束
解释了在具有 `Send` future 的异步 trait 方法中使用 `&self` 如何隐式要求实现类型具有 `Sync`,并提供了诸如改用 `&mut self` 或使用 `Sync` 内部可变性等解决方法。
安全变得简单 第1部分:单一所有权(并非)可选
本文介绍了一种基于线性类型和抽象解释的内存安全新方法,旨在比Rust更符合人机工程学原理地消除诸如释放后使用和内存泄漏等常见错误。
谁在运行你的 Rust Future?动手实践入门异步 Rust
这是一套动手实践教程系列,旨在弥合理解异步 Rust 内部机制(Future、poll、Pin、执行器)与使用 Tokio 部署实际异步代码之间的差距,面向熟悉 JavaScript 异步和基础 Rust 的开发者。
面向 Morello 的 Rust:始终在线内存安全,即使在非安全代码中
本文介绍了一种针对 Morello 能力硬件架构修改的 Rust 编译器,通过利用硬件能力,实现即使在非安全代码中也始终在线内存安全。
iddqd:最难的一种不安全Rust
本文介绍了 iddqd,这是一个 Rust 库,它提供了从值中借用键的映射,减少了重复和同步问题。本文讨论了编写不安全 Rust 代码的挑战以及该库如何保持正确性。