• 因为要保证提供零成本抽象(zero cost abstraction)的原则,这意味着抽象不应该引入额外的运行时开销,所以 Rust 选择了第三种方案。也因此,pinningAPI 在RFC2349中被提出。接下来,我们将会对这个 API 进行简要介绍,并解释它是如何与 async/await 以及 future 一同工作的。

    堆上的值(Heap Values)

    第一个发现是,在大多数情况下,堆分配(heap allocated)的值已经在内存中有了一个固定地址。它们通过调用allocate来创建,然后被一个指针类型引用,比如Box<T>。尽管指针类型有可能被移动,但是指针指向的堆上的值仍然保持在相同的内存地址,除非它被一个deallocate调用来释放。

    使用堆分配,我们可以尝试去创建一个自引用结构体:

    fn main() {
        let mut heap_value = Box::new(SelfReferential {
            self_ptr: 0 as *const _,
        });
        let ptr = &*heap_value as *const SelfReferential;
        heap_value.self_ptr = ptr;
        println!("heap value at: {:p}", heap_value);
        println!("internal reference: {:p}", heap_value.self_ptr);
    }

    struct SelfReferential {
        self_ptr: *const Self,
    }

    在 playground 上运行代码

    我们创建了一个名为SelfReferential的简单结构体,该结构体仅包含一个单独的指针字段。首先,我们使用一个空指针来初始化这个结构体,然后使用Box::new在堆上分配它。接着,我们计算出这个分配在堆上的结构体的内存地址并将其存储到一个ptr变量中。最后,我们通过把ptr变量赋值给self_ptr字段使得结构体成为自引用的。

    当我们在 playground 上执行这段代码时,我们看到这个堆上的值的地址和它的内部指针的地址是相等的,这意味着,self_ptr字段是一个有效的自引用。因为heap_value只是一个指针,移动它(比如,把它作为参数传入函数)不会改变结构体自身的值,所以self_ptr在指针移动后依然是有效的。

    但是,仍然有一种方式来破坏这个示例:我们可以摆脱Box<T>或者替换它的内容:

    let stack_value = mem::replace(&mut *heap_value, SelfReferential {
        self_ptr: 0 as *const _,
    });
    println!("value at: {:p}", &stack_value);
    println!("internal reference: {:p}", stack_value.self_ptr);

    在 playground 上运行

    这里,我们使用mem::replace函数使用一个新的结构体实例来替换堆分配的值。这使得我们把原始的heap_value移动到栈上,而结构体的self_ptr字段现在是一个仍然指向旧的堆地址的悬垂指针。当你尝试在 playground 上运行这个示例时,你会看到打印出的"value at:""internal reference:"这一行确实是输出的不同的指针。因此,在堆上分配一个值并不能保证自引用的安全。

    出现上面的破绽的基本问题是,Box<T>允许我们获得堆分配值的&mut T引用。这个&mut引用让使用类似mem::replace或者mem::swap的方法使得堆上值失效成为可能。为了解决这个问题,我们必须阻止创建对自引用结构体的&mut引用。

    Pin<Box

    pinning API 以Pin包装类型和Unpin标记 trait 的形式提供了一个针对&mut T问题的解决方案。这些类型背后的思想是对Pin的所有能被用来获得对 Unpin trait 上包装的值的&mut引用的方法(如get_mut或者deref_mut)进行管控。Unpin trait 是一个auto trait,它会为所有的类型自动实现,除了显式选择退出(opt-out)的类型。通过让自引用结构体选择退出Unpin,就没有(安全的)办法从一个Pin<Box<T>>类型获取一个&mut T。因此,它们的内部的自引用就能保证仍是有效的。

    举个例子,让我们修改上面的SelfReferential类型来选择退出Unpin

    use core::marker::PhantomPinned;

    struct SelfReferential {
        self_ptr: *const Self,
        _pin: PhantomPinned,
    }

    我们通过添加一个类型为PhantomPinned_pin字段来选择退出。这个类型是一个零大小标记类型,它唯一目的就是不去实现Unpin trait。因为 auto trait 的工作方式,有一个字段不满足Unpin,那么整个结构体都会选择退出Unpin

    第二步是把例子中的Box<SelfReferential>改为Pin<Box<SelfReferential>>类型。实现这个的最简单的方式是使用Box::pin函数,而不是使用Box::new创建堆分配的值。

    let mut heap_value = Box::pin(SelfReferential {
        self_ptr: 0 as *const _,
        _pin: PhantomPinned,
    });

    除了把Box::new改为Box::pin之外,我们还需要在结构体初始化添加新的_pin字段。因为PhantomPinned是一个零大小类型,我们只需要它的类型名来初始化它。

    当我们尝试运行调整后的示例时,我们看到它无法编译:

    error[E0594]: cannot assign to data in a dereference of `std::pin::Pin<std::boxed::Box<SelfReferential>>`
      --> src/main.rs:10:5
       |
    10 |     heap_value.self_ptr = ptr;
       |     ^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign
       |
       = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin<std::boxed::Box<SelfReferential>>`

    error[E0596]: cannot borrow data in a dereference of `std::pin::Pin<std::boxed::Box<SelfReferential>>` as mutable
      --> src/main.rs:16:36
       |
    16 |     let stack_value = mem::replace(&mut *heap_value, SelfReferential {
       |                                    ^^^^^^^^^^^^^^^^ cannot borrow as mutable
       |
       = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin<std::boxed::Box<SelfReferential>>`

    两个错误发生都是因为Pin<Box<SelfReferential>>类型没有实现DerefMut trait。这也正是我们想要的,因为DerefMut trait 将会返回一个&mut引用,这是我们想要避免的。发生这种情况是因为我们选择退出了Unpin并把Box::new改为了Box::pin

    现在的问题在于,编译器不仅阻止了第 16 行的移动类型,还禁止了第 10 行的self_ptr的初始化。这会发生时因为编译器无法区分&mut引用的有效使用和无效使用。为了能够正常初始化,我们不得不使用不安全的get_unchecked_mut方法:

    // safe because modifying a field doesn't move the whole struct
    unsafe {
        let mut_ref = Pin::as_mut(&mut heap_value);
        Pin::get_unchecked_mut(mut_ref).self_ptr = ptr;
    }

    尝试在 playground 上运行

    get_unchecked_mut函数作用于Pin<&mut T>而不是Pin<Box<T>>,所以我们不得不使用Pin::as_mut来对之前的值进行转换。接着,我们可以使用get_unchecked_mut返回的&mut引用来设置self_ptr字段。

    现在,生下来的唯一的错误是mem::replace上的期望错误。记住,这个操作试图把一个堆分配的值移动到栈上,这将会破坏存储在self_ptr字段上的自引用。通过选择退出Unpin和使用Pin<Box<T>>,我们可以在编译期阻止这个操作,从而安全地使用自引用结构体。正如我们所见,编译器无法证明自引用的创建是安全的,因此我们需要使用一个不安全的块(block)并且确认其自身的正确性。

    栈 Pinning 和 Pin<&mut T>

    在先前的部分,我们学习了如何使用Pin<Box<T>>来安全地创建一个堆分配的自引用的值。尽管这种方式能够很好地工作并且相对安全(除了不安全的构造),但是需要的堆分配也会带来性能损耗。因为 Rust 一直想要尽可能地提供零成本抽象, 所以 pinning API 也允许去创建Pin<&mut T>实例指向栈分配的值。

    不像Pin<Box<T>> 实例那样能够拥有被包装的值的所有权,Pin<&mut T>实例只是暂时地借用被包装的值。这使得事情变得更加复杂,因为它要求程序员自己确认额外的保证。最重要的是,一个Pin<&mut T> 必须在被引用的T的整个生命周期被保持 pinned,这对于栈上的变量很难确认。为了帮助处理这类问题,就有了像pin-utils这样的 crate。但是我仍然不会推荐 pinning 到栈上除非你真的知道自己在做什么。

    想要更加深入地了解,请查阅pin 模块Pin::new_unchecked方法的文档。

    Pinning 和 Futures

    正如我们在本文中已经看到的,Future::poll方法以Pin<&mut Self>参数的形式来使用 pinning:

    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>

    这个方法接收self: Pin<&mut Self>而不是普通的&mut self,其原因在于,从 async/await 创建的 future 实例常常是自引用的。通过把Self包装进Pin并让编译器为由 async/await 生的自引用的 futures 选择退出Unpin,可以保证这些 futures 在poll调用之间在内存中不被移动。这就保证了所有的内部引用都是仍然有效的。

    值得注意的是,在第一次poll调用之前移动 future 是没问题的。因为事实上 future 是懒惰的(lazy)并且直到它们被第一次轮询之前什么事情也不会做。生成的状态机中的start状态因此只包含函数参数,而没有内部引用。为了调用poll,调用者必须首先把 future 包装进Pin,这就保证了 future 在内存中不会再被移动。因为栈上的 pinning 难以正确操作,所以我推荐一直使用Box::pin组合Pin::as_mut

    如果你想了解如何安全地使用栈 pinning 实现一个 future 组合字函数,可以去看一下map 组合子方法的源码,以及 pin 文档中的 projections and structural pinning部分

    02-02 08:25