深入理解 Rust 的 Pin 和 Unpin:理论与实践解析

深入理解Rust的Pin和Unpin:理论与实践解析在Rust的异步编程中,Pin和Unpin是两个核心概念,它们决定了对象是否可以在内存中移动。本篇文章将深入探讨Pin的工作原理及其背后的设计逻辑,帮助读者更好地理解和使用这些工具以编写更安全和高效的代码。Pin和U

深入理解 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&lt;&mut Self>) {
        let self_ptr: *const String = &self.a;
        let this = unsafe { self.get_unchecked_mut() };
        this.b = self_ptr;
    }

    fn a(self: Pin&lt;&Self>) -> &str {
        &self.get_ref().a
    }

    fn b(self: Pin&lt;&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&lt;Box&lt;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&lt;&Self>) -> &str {
        &self.get_ref().a
    }

    fn b(self: Pin&lt;&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&lt;'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 探索更多深度思考与精彩内容,欢迎关注我的公众号【寻月隐君】!在这里,我们将一同探寻那些隐藏在月光下的故事与智慧。 扫描下面二维码关注寻月隐君公众号

  • 原创
  • 学分: 4
  • 分类: Rust
  • 标签: Rust 
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
寻月隐君
寻月隐君
0x750E...B6f5
不要放弃,如果你喜欢这件事,就不要放弃。如果你不喜欢,那这也不好,因为一个人不应该做自己不喜欢的事。