深入理解 Rust 的 Pin 和 Unpin:理论与实践解析
深入理解 Rust 的 Pin 和 Unpin:理论与实践解析
在 Rust 的异步编程中,Pin 和 Unpin 是两个核心概念,它们决定了对象是否可以在内存中移动。本篇文章将深入探讨 Pin 的工作原理及其背后的设计逻辑,帮助读者更好地理解和使用这些工具以编写更安全和高效的代码。
Pin 和 Unpin 是 Rust 中与内存安全密切相关的特性。通过 Pin,可以将对象固定在内存中的特定位置,防止移动可能导致的引用失效问题。而 Unpin 则表示对象可以安全移动。本篇文章首先分析了异步代码生成的 Future 的内部结构,然后深入讲解了 Pin 的原理、Unpin 特性及其实践应用,包括在堆上固定对象、标记类型 PhantomPinned 的作用,以及如何在实际代码中避免移动敏感数据。此外,文章通过详细代码示例演示了 Pin 和 Unpin 的实际使用场景。
Pinning
什么是 Pin
- Pin 与 Unpin 标记一起工作
- Pin 会保证实现了 !Unpin 的对象永远不会被移动
为什么需要 Pin?
let fut_one = /* ... */; // Future 1
let fut_two = /* ... */; // Future 2
async move {
fut_one.await;
fut_two.await;
}
- 这会创建一个实现了 Future trait 的匿名类型
- 提供一个和下面代码类似的 poll 方法
// The `Future` type generated by our `async { ... }` block
// `async { ... }`语句块创建的 `Future` 类型
struct AsyncFuture {
fut_one: FutOne,
fut_two: FutTwo,
state: State,
}
// List of states our `async` block can be in
// `async` 语句块可能处于的状态
enum State {
AwaitingFutOne,
AwaitingFutTwo,
Done,
}
impl Future for AsyncFuture {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
loop {
match self.state {
State::AwaitingFutOne => match self.fut_one.poll(..) {
Poll::Ready(()) => self.state = State::AwaitingFutTwo,
Poll::Pending => return Poll::Pending,
}
State::AwaitingFutTwo => match self.fut_two.poll(..) {
Poll::Ready(()) => self.state = State::Done,
Poll::Pending => return Poll::Pending,
}
State::Done => return Poll::Ready(()),
}
}
}
}
当 poll
第一次被调用时,它会去查询 fut_one
的状态,若 fut_one
无法完成,则 poll
方法会返回。未来对 poll
的调用将从上一次调用结束的地方开始。该过程会一直持续,直到 Future
完成为止。
如果上例中 async 块使用引用,会如何?
async {
let mut x = [0; 128];
let read_into_buf_fut = read_into_buf(&mut x);
read_into_buf_fut.await;
println!("{:?}", x);
}
这段代码会编译成下面的形式:
struct ReadIntoBuf<'a> {
buf: &'a mut [u8], // 指向下面的`x`字段
}
struct AsyncFuture {
x: [u8; 128],
read_into_buf_fut: ReadIntoBuf<'what_lifetime?>,
}
这里,ReadIntoBuf
拥有一个引用字段,指向了结构体的另一个字段 x
,一旦 AsyncFuture
被移动,那 x
的地址也将随之变化,此时对 x
的引用就变成了不合法的。
- 把 Future Pin(钉)到内存中的特定位置会防止该问题的发生:
- 可以在 async 块里安全的创建到值的引用
Pin 介绍
#[derive(Debug)]
struct Test {
a: String,
b: *const String,
}
impl Test {
fn new(txt: &str) -> Self {
Test {
a: String::from(txt),
b: std::ptr::null(),
}
}
fn init(&mut self) {
let self_ref: *const String = &self.a;
self.b = self_ref;
}
fn a(&self) -> &str {
&self.a
}
fn b(&self) -> &String {
assert!(!self.b.is_null(), "Test::b called without Test::init being called first");
unsafe { &*(self.b) }
}
}
fn main() {
let mut test1 = Test::new("test1");
test1.init();
let mut test2 = Test::new("test2");
test2.init();
println!("a: {}, b: {}", test1.a(), test1.b());
println!("a: {}, b: {}", test2.a(), test2.b());
}
运行输出
a: test1, b: test1
a: test2, b: test2
修改之后
fn main() {
let mut test1 = Test::new("test1");
test1.init();
let mut test2 = Test::new("test2");
test2.init();
println!("a: {}, b: {}", test1.a(), test1.b());
std::mem::swap(&mut test1, &mut test2); // 交换 test1 和 test2 移动数据
println!("a: {}, b: {}", test2.a(), test2.b());
}
输出
a: test1, b: test1
a: test1, b: test2
Pin 的实践
- Pin 类型会包裹指针类型,保证指针指向的值不被移动。
- 例如:Pin<&mut T>,Pin<&T>, Pin<Box<T>>
- 即使 T:!Unpin,也不能保证 T 不被移动
Unpin trait
- 大多数类型如果被移动,不会造成问题,它们实现了 Unpin
- 指向 Unpin 类型的指针,可自由的放入或从 Pin 中取出
- 例如:u8 是 Unpin 的,Pin<&mut u8> 和普通的 &mut u8 一样
- 如果类型拥有 !Unpin 标记,那么在 Pin 之后它们就无法移动了
use std::pin::Pin;
use std::marker::PhantomPinned;
#[derive(Debug)]
struct Test {
a: String,
b: *const String,
_marker: PhantomPinned,
}
impl Test {
fn new(txt: &str) -> Self {
Test {
a: String::from(txt),
b: std::ptr::null(),
_marker: PhantomPinned, // 这个标记可以让我们的类型自动实现特征`!Unpin`
}
}
fn init(self: Pin<&mut Self>) {
let self_ptr: *const String = &self.a;
let this = unsafe { self.get_unchecked_mut() };
this.b = self_ptr;
}
fn a(self: Pin<&Self>) -> &str {
&self.get_ref().a
}
fn b(self: Pin<&Self>) -> &String {
assert!(!self.b.is_null(), "Test::b called without Test::init being called first");
unsafe { &*(self.b) }
}
}
上面代码中,我们使用了一个标记类型 PhantomPinned
将自定义结构体 Test
变成了 !Unpin
(编译器会自动帮我们实现),因此该结构体无法再被移动。
一旦类型实现了 !Unpin
,那将它的值固定到栈( stack
)上就是不安全的行为,因此在代码中我们使用了 unsafe
语句块来进行处理,你也可以使用 pin_utils
来避免 unsafe
的使用。
pub fn main() {
// test1 is safe to move before we initialize it
// 此时的`test1`可以被安全的移动
let mut test1 = Test::new("test1");
// Notice how we shadow `test1` to prevent it from being accessed again
// 新的`test1`由于使用了`Pin`,因此无法再被移动,这里的声明会将之前的`test1`遮蔽掉(shadow)
let mut test1 = unsafe { Pin::new_unchecked(&mut test1) };
Test::init(test1.as_mut());
let mut test2 = Test::new("test2");
let mut test2 = unsafe { Pin::new_unchecked(&mut test2) };
Test::init(test2.as_mut());
println!("a: {}, b: {}", Test::a(test1.as_ref()), Test::b(test1.as_ref()));
println!("a: {}, b: {}", Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}
尝试进行交换
pub fn main() {
let mut test1 = Test::new("test1");
let mut test1 = unsafe { Pin::new_unchecked(&mut test1) };
Test::init(test1.as_mut());
let mut test2 = Test::new("test2");
let mut test2 = unsafe { Pin::new_unchecked(&mut test2) };
Test::init(test2.as_mut());
println!("a: {}, b: {}", Test::a(test1.as_ref()), Test::b(test1.as_ref()));
std::mem::swap(test1.get_mut(), test2.get_mut()); // 报错
println!("a: {}, b: {}", Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}
Pinning to the Heap 固定到堆上
use std::pin::Pin;
use std::marker::PhantomPinned;
#[derive(Debug)]
struct Test {
a: String,
b: *const String,
_marker: PhantomPinned,
}
impl Test {
fn new(txt: &str) -> Pin<Box<Self>> {
let t = Test {
a: String::from(txt),
b: std::ptr::null(),
_marker: PhantomPinned,
};
let mut boxed = Box::pin(t);
let self_ptr: *const String = &boxed.a;
unsafe { boxed.as_mut().get_unchecked_mut().b = self_ptr };
boxed
}
fn a(self: Pin<&Self>) -> &str {
&self.get_ref().a
}
fn b(self: Pin<&Self>) -> &String {
unsafe { &*(self.b) }
}
}
pub fn main() {
let test1 = Test::new("test1");
let test2 = Test::new("test2");
println!("a: {}, b: {}",test1.as_ref().a(), test1.as_ref().b());
println!("a: {}, b: {}",test2.as_ref().a(), test2.as_ref().b());
}
将一个 !Unpin
类型的值固定到堆上,会给予该值一个稳定的内存地址,它指向的堆中的值在 Pin
后是无法被移动的。而且与固定在栈上不同,我们知道堆上的值在整个生命周期内都会被稳稳地固定住。
总结
-
如果 T:Unpin (默认情况),那么 Pin<'a, T> 与 &'a mut T 完全等价
- Unpin 意味着该类型如果被 Pin 了,那么它也是可以移动的,所以 Pin 对这种类型不起作用
-
如果 T:!Unpin,那么把 &mut T 变成 Pin 的 T,需要 unsafe 操作
-
大多数标准库类型都实现了 Unpin,Rust 里大部分正常类型也是。由 async/await 生成的 Future 是个例外
-
可以使用特性标记为类型添加一个 !Unpin 绑定(最新版),或者通过添加
std::marker::PhantomPinned
到类型上(稳定版) -
可以将数据 Pin 到 Stack 或 Heap 上
-
把 !Unpin 对象 Pin 到 Stack 上需要 unsafe 操作
-
把 !Unpin duix Pin 到 Heap 上不需要 unsafe 操作
- 快捷操作:使用 Box::pin
-
针对已经 Pin 的数据,如果它是 T: !Unpin 的,则需要保证它从被 Pin 后,内存一直有效且不会调整其用途,直到 drop 被调用。
- 这是 Pin 合约的重要部分
-
若
T: Unpin
( Rust 类型的默认实现),那么Pin<'a, T>
跟&'a mut T
完全相同,也就是Pin
将没有任何效果, 该移动还是照常移动 -
绝大多数标准库类型都实现了
Unpin
,事实上,对于 Rust 中你能遇到的绝大多数类型,该结论依然成立 ,其中一个例外就是:async/await
生成的Future
没有实现Unpin
-
你可以通过以下方法为自己的类型添加
!Unpin
约束:- 使用文中提到的
std::marker::PhantomPinned
- 使用
nightly
版本下的feature flag
- 使用文中提到的
-
可以将值固定到栈上,也可以固定到堆上
- 将
!Unpin
值固定到栈上需要使用unsafe
- 将
!Unpin
值固定到堆上无需unsafe
,可以通过Box::pin
来简单的实现
- 将
-
当固定类型
T: !Unpin
时,你需要保证数据从被固定到被drop这段时期内,其内存不会变得非法或者被重用
总结
本篇文章通过理论与实践结合的方式全面解析了 Rust 中 Pin 和 Unpin 的设计目的及使用方法。在异步编程、引用安全及内存管理等场景中,Pin 提供了一种固定数据位置的能力,避免了潜在的内存安全问题。同时,文章结合 PhantomPinned 的使用,展示了如何构建不可移动类型,进而提升程序的健壮性。理解和熟练应用这些概念将帮助开发者更好地编写高性能和安全的 Rust 程序。
https://github.com/qiaopengjun5162 探索更多深度思考与精彩内容,欢迎关注我的公众号【寻月隐君】!在这里,我们将一同探寻那些隐藏在月光下的故事与智慧。 扫描下面二维码关注寻月隐君公众号
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。