Rust中的Pin和Unpin
简介
Pin<P>
是一个struct
,Pin<P>
的作用是将P所指向内存对象固定住,不能通过Safe的方式进行移动。不能通过safe代码拿到
&mut T
。
Pin的由来
Rust中Pin
出现的初衷是为了解决异步运行时中的状态机自引用结构体如果被移动后带来的问题而设计的。
先了解一下自引用结构体。
自引用结构体(Self-Reference-Struct)
自引用结构体是指在内部包含了指向自身某个内存位置指针的结构体;
自引用结构体在移动时,内部的自引用指针并无移动,导致移动后的自引用指针仍然指向原来的位置,变为悬垂指针,从而产生内存安全问题,违反rust的设计初衷;
Rust通过引入
Pin
来防止自引用结构体被随意(Safe方式)移动,来解决自引用结构体
移动带来的问题。
|
|
Pin
Pin<P>
是一个struct
,属于rust智能指针的一种特殊类型;Pin<P>
的作用:将P
所指向结构体对象钉(Pin)
在内存固定地方,是程序无法通过Safe的方式对被Pin住的对象进行移动;其根本方式是:不能通过safe方式获取结构体的可变引用指针
&mut T
。
Pin
的定义如下:
|
|
Unpin
Unpin
是一个marker trait
;如果一个
T: Unpin
,则T可以被安全的移动(就是可以拿到&mut T
);Unpin
代表对象不存在 self-reference 等情况,可以被随意移动
|
|
!Unpin
对Unpin取反,!Unpin的双重否定就是pin。如果一个类型中包含了PhantomPinned,那么这个类型就是!Unpin
。
|
|
Pin<P>
的实现
我们这里只关注safe方法,重点是new方法:
|
|
可以看出,只有P所指向的T: Unpin
,才可以new出一个Pin<P<T>>
。这里的T就是应该被pin的实例,可是由于T: Unpin
实际上T的实例并不会被pin。 也就是说,T没有实现Unpin
trait时,T才会被真正的pin住。
由于Pin::new
方法要求T: Unpin
,通常创建一个不支持Unpin的T的pin实例的方法是用Box::pin
方法,定义如下:
|
|
例如,自定义了Node结构,如下的代码生成pin实例:
|
|
Node没有实现Unpin
时,通过Pin的安全方法都不能得到&mut Node
,所以就不能移动Node实例。注意,这里是不能移动Node实例,node_pined是Pin实例,是可以移动的。
当然,通过Pin的unsafe方法,仍然可以得到mut Node
,也可以移动Node实例,但这些unsafe的操作就需要程序员自己去承担风险。Pin相关方法中对此有很详细的说明。
Pin可以被看作一个限制指针(Box<T>
或&mut T
)的结构,在T: Unpin
的情况下,Pin<Box<T>>
和Box<T>
是类似的,通过DerefMut
就可以直接得到&mut T
,在T没有实现Unpin的情况下,Pin<Box<T>>
只能通过Deref
得到&T
,就是说T被pin住了。
Pin这种自废武功的方法怪怪的,为什么要有Pin?
虽然Box、Rc、Arc等指针类型也可以让实例在heap中固定,但是这些指针的safe方法会暴露出&mut T
,这就会导致T的实例被移动,比如通过std::mem::swap
方法,也可以是Option::take
方法,还可能是Vec::set_len
、Vec::resize
方法等,这些可都是safe等方法。
这些方法的共同点都是需要&mut Self
,所以说只要不暴露&mut Self
,就可以达到pin的目标。
当把一个值放进 Pin 中,就无法再获取这个值的可变引用。这样就无法通过赋值、传参、返回、方法调用、
std::mem::replace
这些方法来移动值了。当然也有例外,当你明确地知道被 Pin 包裹的值不会再被移动,那可以通过 unsafe 的方法来获取它的可变引用。
为什么需要pin?
事情的起因就是Async/.Await异步编程的需要。
看看如下异步编程的代码:
|
|
rustc在编译是会自动生成类似如下的代码,其中的AsyncFuture
会是一个自引用结构:
|
|
注意Future::poll
方法的第一个参数是Pin<&mut Self>
,如果在Future::poll
方法中有类似std::mem::swap
等方法调用,就有可能导致AsyncFuture
被移动,那么AsyncFuture中的自引用field就会导致灾难。
注意到Future::poll
代码是自动生成的,可以不调用std::mem::swap
等方法,就不会导致AsyncFuture被移动。的确是这样的,如果在这里将Future::poll
的第一个参数改为Box<Self>
或者&mut Self
,大概率是没有问题的。很多executor的实现,都是要求Future是支持Unpin,因为在poll代码中的确有修改Self的需求,但不会产生错误,也是这个原因。
但是,对于程序员实现Future的情况,问题就来了。
如果poll的参数是&mut Self
,那么程序员就可能使用safe代码(比如std::mem::swap)产生错误,这是与rust安全编码的理念相冲突的。这就是Pin引入的根本原因!
其实,在future 0.1版本中,poll的这个参数就是&mut Self
,如下:
|
|
总结
Pin实际是对P指针的限制,在T没有实现Unpin的情况下,避免P指针暴露
&mut Self
。Pin的引入是Async/.Await异步编程的需要,核心就是
Future::poll
方法参数的需要。除了
Future::poll
方法之外,不建议使用Pin,也没有必要使用Pin.