Rust异步4: Pin和自指类型
在前面介绍Future模式的文章里, 简单讲了下poll方法里的入参Pin类型, 引入Pin是为了解决自指类型的问题, "固定"Future在内存中的位置. 这篇文章就来介绍自指类型是什么, 和Future有什么关系以及Pin是如何解决问题的这些内容.
自指类型是什么, 会有什么问题
struct SelfReference {
data: i32,
// pointer指向data
pointer: * const i32
}
let mut a1 = SelfReference { data: 1, pointer: std::ptr::null() };
// a1.pointer 指向了 a1.data
a1.pointer = &a1.data;
// SelfReference { data: 1, pointer: 0x7fffffffd9d0 }, 1
unsafe { println!("{:?}, {}", a1, *(a1.pointer)); }
自指类型也叫做自引用类型, 指的是包含指向自身空间的指针的类型. 也就是说, 自指类型的实例里会有指针字段, 这个指针指向的地址又是和实例本身相关的. 比如上面的SelfReference就是自指类型, pointer字段指向了data字段, 把pointer指向的内容和data打印出来的结果是一样的.
let mut a1 = SelfReference { data: 1, pointer: std::ptr::null() };
a1.pointer = &a1.data;
// SelfReference { data: 1, pointer: 0x7fffffffd7f0 }, 1
unsafe { println!("{:?}, {}", a1, *(a1.pointer)); }
let mut a2 = SelfReference { data: 0, pointer: std::ptr::null() };
a2.pointer = &a2.data;
// SelfReference { data: 0, pointer: 0x7fffffffd850 }, 0
unsafe { println!("{:?}, {}", a2, *(a2.pointer)); }
// 交换 a1 和 a2 的内容, 相当于 a1 和 a2 的内存地址变更了
std::mem::swap(&mut a1, &mut a2);
// SelfReference { data: 0, pointer: 0x7fffffffd850 }, 1
unsafe { println!("{:?}, {}", a1, *(a1.pointer)); }
// SelfReference { data: 1, pointer: 0x7fffffffd7f0 }, 0
unsafe { println!("{:?}, {}", a2, *(a2.pointer));
当实例在内存中的位置移动(比特复制)时, 自指类型就会出现问题, 比如swap
交换上面的a1和a2, 把a1的数据移到了a2的位置, a2的数据移到了a1的位置. a1中现在data是0, 而pointer指向的内容还是1, 因为pointer指向的不是"自己"的data了, a2也是同样的情况.
一方面, 会导致实例在内存中被移动的情况很常见, 比如函数入参, 数组扩容, swap, Box::new等等; 另一方面, 虽然自指类型不算常见, 但Rust并不限制我们定义自指类型, 而且在Future中出现了自指类型的身影. 所以对于这种可能导致未定义行为的陷阱, Rust应该会觉得有必要做些什么.
自指类型和Future有什么关系
async fn foo() {
let a = 1;
let b = &a;
// 任务在这会让出执行
future1().await;
// a和b是跨await的变量
let c = a + *b;
}
之前的文章说到编译器会把异步函数转换成Future
实现, 指令和数据信息都会保存在状态机中. 在上面的异步函数中, a和b是跨域await点的变量, 状态机在下次继续执行时需要用到这两个变量, 所以a和b需要保存在状态机中, b又是指向a的指针, 所以编译器生成的匿名Future就会是自指类型.
不仅是Future, 各种各样的迭代器, 还有正在开发中的生成器, 都是用状态机来实现的. 如果这些状态机要维护的状态里, 有指向自身的指针字段, 那就变成了自指类型, 都会存在内存移动时出现问题. 由于历史原因, 迭代器Iterator的next方法里并不是Pin, 这和Future的poll方法不同, 但最近实现的生成器Coroutine的resume方法则已引入了Pin.
Pin的定义和工作原理
虽然显式的自指类型并不常见, 但Future, Iterator和Coroutine这些类型对Rust来说是如此重要, 所以自指类型会造成的潜的影响是如此广泛. 自指类型出问题发生在内存位置移动时, 所以要解决问题一方面可以从类型入手, 另一方面可以从"移动"入手.
Rust目前选择的方向是处理"移动", 给出的解决方案是Pin. 在开始介绍Pin前, 有必要先介绍下Unpin和!Unpin这两个标记类型, 根据类型实例在内存中移动是否安全, Rust把所有类型归入这两类.
pub auto trait Unpin {}
// 为类型实现Unpin特征
impl Unpin for PinStruct {}
Unpin的意思是不需要固定, 也就是说实现了Unpin的类型在内存中移动是安全的. 几乎能想到的类型, 比如i32, char, String等等, 都是Unpin类型. 因为在内存中移动Unpin类型不会导致未定义行为, 所以Unpin不需要Pin提供额外的保障, Pin对于Unpin类型的影响就像透明一样.
// 方式1: 显示实现 !Unpin
struct NotUnPinStruct1;
impl !Unpin for NotUnPinStruct1 {}
// 方式2: 包含 !Unpin 的字段
struct NotUnPinStruct2 {
_marker: PhantomPinned
}
!Unpin与Unpin相反, 意思是需要固定, 实现了!Unpin的类型在内存中移动是不安全的. !Unpin类型并不常见, 有可能会遇见的, 一是上面的Future这类状态机, 另一个是标记类型PhantomPinned. 我们要自定义!Unpin类型的话, 可以手动为类型实现!Unpin, 也可以在类型里包含!Unpin的字段. 因为在内存中移动!Unpin类型会导致未定义行为, 所以Pin方案要处理的主要就是!Unpin类型.
pub struct Pin<Ptr> {
pub __pointer: Ptr,
}
Pin的完整写法是Pin<P<T>>
, 其中Pin是"外层"指针, P是"中级"的指针. Pin<P<T>>
给出的承诺是, 如果使用者按约定构造了Pin, 那么从T被Pin住开始到被释放, T在内存中的位置都不会改变(T为Unpin时不限制).
仅借助Rust的语法是没办法实现Pin应该提供的承诺的, 所以Pin需要使用者遵守一定的约定, 约定的内容针对"中级"的P指针的, 有两条:
- P指针的解引用方法
deref
和deref_mut
不能移动T, 因为Pin的as_ref
和as_mut
会依赖于这两个方法 - 当
Pin<P<T>>
被释放后, P指针指向的T也不能再被移动. 因为Pin只能在自己的生命周期内起作用, 所以超出Pin生命周期的"承诺"需要靠"约定"来实现.
// T 是 Unpin 类型, 方法是安全的
impl<P: Deref<Target: Unpin>> Pin<P> {
pub const fn new(pointer: P) -> Pin<P> { unsafe { Pin::new_unchecked(pointer) }
}
// T 可以是 !Unpin 的, 方法是不安全的
impl<P: Deref> Pin<P> {
pub const unsafe fn new_unchecked(pointer: P) -> Pin<P> { Pin { pointer }
}
所以Pin提供了两个构造方法, 一个是针对Unpin的new
方法, 是safe的, 因为Unpin在内存中移动是安全的, 不需要Pin提供额外的保障; 另一个是针对!Unpin的new_unchecked
方法, 是unsafe的, 因为使用这个方法需要开发者遵守上面的约定, 这边没法通过Rust语法来确保正确性.
归纳一下在内存中移动T的情况, 可以分成两种类型, 一是有T的所有权, 二是有T的可变引用&mut T
. 而在Rust中能拿到变量的所以权时, 也就可以间接拿到拿到可变引用, 所以这两种情况可以合而为一, 即要想在内存中移动T, 就要能拿到T的可变引用&mut T
.
前文讲到Pin是从"移动"方面入手来解决自指类型的问题, 所以Pin的思路就是构造出Pin<P<T>>
后, 就不再通过安全方法暴露&mut T
, 下面就来看Pin提供给开发者的那些方法.
// T 是 Unpin 类型, 方法是安全的
impl<P: Deref<Target: Unpin>> Pin<P> {
pub const fn into_inner(pin: Pin<P>) -> P { pin.pointer }
}
// T 可以是 !Unpin 的, 方法是不安全的
impl<P: Deref> Pin<P> {
pub const unsafe fn into_inner_unchecked(pin: Pin<P>) -> P { pin.pointer }
}
首先是获取P<T>
的两个方法, 针对Unpin类型的into_inner
是安全的, 因为Unpin本身就可以安全地移动, 直接把P<Unpin>
返回出去不会有问题. 而针对!Unpin类型的into_inner_unchecked
是不安全的, 因为不确定P<!Unpin>
会不会把&mut T
暴露出去, 所以需要开发者自己保证.
// 如果 P 实现了 Deref, Pin<P<T>> -> Pin<& T>
impl<P: Deref> Pin<P> {
pub fn as_ref(&self) -> Pin<&P::Target> { unsafe { Pin::new_unchecked(&*self.pointer) } }
}
// 如果 P 实现了 DerefMut, Pin<P<T>> -> Pin<&mut T>
impl<P: DerefMut> Pin<P> {
pub fn as_mut(&mut self) -> Pin<&mut P::Target> { unsafe { Pin::new_unchecked(&mut *self.pointer) } }
}
接着是把Pin<p<T>>
转换到Pin<&T>
的方法, 即把中间这一层可能是各种类型的P指针, 转换到统一的引用&
形式. 根据P是否是可变的, 有as_ref
和as_mut
两个方法. 这两个方法都是安全的, 因为从Pin<&T>
拿不到T的可变引用.
// Pin<P<T>>调用as_ref转换成Pin<&T>后就可以调用
impl<'a, T: ?Sized> Pin<&'a T> {
pub const fn get_ref(self) -> &'a T { self.pointer }
pub unsafe fn map_unchecked<U, F>(self, func: F) -> Pin<&'a U>
where U: ?Sized, F: FnOnce(&T) -> &U
{
let pointer = &*self.pointer;
let new_pointer = func(pointer);
unsafe { Pin::new_unchecked(new_pointer) }
}
}
然后是Pin<& T>
的两个方法. 第一个方法get_ref
是把Pin这一层去掉返回&T
, 因为无法通过不可变引用移动T, 所以这个方法是安全的. 第二个方法map_unchecked
用来做映射, 这边因为入参func是不可控的, 所以func(pointer)
得到的new_pointer
不一定会遵守Pin的约定, 所以只能通过不安全的new_unchecked
来构造Pin, 所以这个方法也是不安全的.
// Pin<P<T>>调用as_mut转换成Pin<&mut T>后就可以调用
impl<'a, T: ?Sized> Pin<&'a mut T> {
pub const fn get_mut(self) -> &'a mut T where T: Unpin { self.pointer }
pub const unsafe fn get_unchecked_mut(self) -> &'a mut T { self.pointer }
pub unsafe fn map_unchecked_mut<U, F>(self, func: F) -> Pin<&'a mut U>
where U: ?Sized, F: FnOnce(&mut T) -> &mut U { ... }
pub const fn into_ref(self) -> Pin<&'a T> { Pin { pointer: self.pointer } }
}
最后是Pin<&mut T>
的几个方法. get_mut
, get_unchecked_mut
和上面的get_ref
一样都是去掉Pin这一层, 但因为返回的是可变引用&mut T
, 所以根据T是Unpin还是!Unpin, 分成了安全方法get_mut
和不安全方法get_unchecked_mut
. map_unchecked_mut
是map_unchecked
的可变版本, 也是不安全的, 理由map_unchecked
. into_ref
方法则是扩展as_ref
, 把Pin<&mut T>
也转换到Pin<&T>
, 从可变到不可变, 这个方法是安全的.
常见的Pin<P<T>>
类型
-
Pin<&mut T>
fn move_pinned_ref<T>(mut a: T, mut b: T) { unsafe { // 通过可变引用 pin 住 a let p: Pin<&mut T> = Pin::new_unchecked(&mut a); } // 还可以再次拿到 &mut a, 不满足 pin 的约定 std::mem::swap(&mut a, &mut b); }
通过
&mut T
构造的Pin<&mut T>
并不满足第二个约定条件, 比如下面的示例代码, 在释放掉p后依然可以拿到&mut a
, 从而可以移动a指向数据的位置. 尽管不满足Pin的约定, 但这段代码是可以通过编译的, 只是超出Pin生命周期后的承诺无法兑现. 对Rust来说, 这可能会导致库层面的未定义行为, 但不是语言层面的未定义行为, 所以在这里并不能提供更多的帮助, T可能是!Unpin的, 所以只能用不安全的new_unchecked
方法. -
Pin<Box<T>>
fn pin(x: T) -> Pin<Box<T>> { unsafe { Pin::new_unchecked(Box::new(x)) } }
Box<T>
是满足Pin的两个约定的, 一是Box的解引用方法不会移动内部的T, 二是Pin释放后, Box也会被释放, 里面的T也直接释放掉了. Box提供了一个安全的包装方法, 不管T是Unpin的还是!Unpin的, 都可以直接用Box::pin(T)
固定T. -
Pin<Rc<T>>
// Rc 提供的构造 Pin<Rc<T>> 的方法 fn pin(value: T) -> Pin<Rc<T>> { unsafe { Pin::new_unchecked(Rc::new(value)) } } fn move_pinned_rc<T>(mut a: Rc<T>) { // 通过Rc指针 pin 住 a let pinned = unsafe { Pin::new_unchecked(Rc::clone(&a)) }; { let p: Pin<&T> = pinned.as_ref(); } drop(pinned); // 还可以再次拿到 &mut T, 不满足 pin 的约定 let t: &mut T = Rc::get_mut(&mut a).unwrap(); }
虽然和Box一样, Rc也提供了安全的包装方法, 但
Pin<Rc<T>>
实际上并不太安全, 因为Rc不一定会满足第二点. 如上面的代码所示, Pin被释放后了, Rc也被释放, 但依然可以从其他的Rc拷贝里拿到&mut T
.