深入理解Rust的Pin和Unpin:理论与实践解析在Rust的异步编程中,Pin和Unpin是两个核心概念,它们决定了对象是否可以在内存中移动。本篇文章将深入探讨Pin的工作原理及其背后的设计逻辑,帮助读者更好地理解和使用这些工具以编写更安全和高效的代码。Pin和U
在 Rust 的异步编程中,Pin 和 Unpin 是两个核心概念,它们决定了对象是否可以在内存中移动。本篇文章将深入探讨 Pin 的工作原理及其背后的设计逻辑,帮助读者更好地理解和使用这些工具以编写更安全和高效的代码。
Pin 和 Unpin 是 Rust 中与内存安全密切相关的特性。通过 Pin,可以将对象固定在内存中的特定位置,防止移动可能导致的引用失效问题。而 Unpin 则表示对象可以安全移动。本篇文章首先分析了异步代码生成的 Future 的内部结构,然后深入讲解了 Pin 的原理、Unpin 特性及其实践应用,包括在堆上固定对象、标记类型 PhantomPinned 的作用,以及如何在实际代码中避免移动敏感数据。此外,文章通过详细代码示例演示了 Pin 和 Unpin 的实际使用场景。
let fut_one = /* ... */; // Future 1
let fut_two = /* ... */; // Future 2
async move {
fut_one.await;
fut_two.await;
}
// 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
的引用就变成了不合法的。
#[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
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()));
}
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 完全等价
如果 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 操作
针对已经 Pin 的数据,如果它是 T: !Unpin 的,则需要保证它从被 Pin 后,内存一直有效且不会调整其用途,直到 drop 被调用。
若 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 探索更多深度思考与精彩内容,欢迎关注我的公众号【寻月隐君】!在这里,我们将一同探寻那些隐藏在月光下的故事与智慧。 扫描下面二维码关注寻月隐君公众号
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!