因为要保证提供零成本抽象(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,
}
我们创建了一个名为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);
这里,我们使用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;
}
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部分