Rust中的Pin和Unpin

Rust中的Pin和Unpin

简介

  • Pin<P>是一个struct

  • Pin<P>的作用是将P所指向内存对象固定住,不能移动。

  • 不能通过safe代码拿到&mut T

定义如下:

1
2
3
pub struct Pin<P> {
    pointer: P,
}

Pin的由来

Rust中Pin的出现的初衷是为了解决异步运行时中的状态机自引用结构体移动带来的问题。

先了解一下自引用结构体。

自引用结构体(Self-Reference-Struct)

  • 自引用结构体是指在内部包含了指向自身某个内存位置指针的结构体;

  • 自引用结构体在移动时,内部的自引用指针并无移动,导致移动后的自引用指针仍然指向移动前的位置,变为悬垂指针,从而产生内存安全问题,违反rust的设计初衷。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct SelfRef<'a> {
    value: String,
    // 该引用指向上面的value
    pointer_to_value: &'a str,
}
fn main(){
    let s = "aaa".to_string();
    let v = SelfRef {
        value: s,
        pointer_to_value: &s
    };
}

Unpin

  • Unpin是一个marker trait

  • 如果一个T: Unpin,则T在pin后可以安全的移动(就是可以拿到&mut T

1
pub auto trait Unpin {}

!Unpin

对Unpin取反,!Unpin的双重否定就是pin。如果一个类型中包含了PhantomPinned,那么这个类型就是!Unpin。

1
2
3
4
pub struct PhantomPinned;

#[stable(feature = "pin", since = "1.33.0")]
impl !Unpin for PhantomPinned {}

Pin<P>的实现

我们这里只关注safe方法,重点是new方法:

1
2
3
4
5
impl<P: Deref<Target: Unpin>> Pin<P> {
    pub fn new(pointer: P) -> Pin<P> {
        unsafe { Pin::new_unchecked(pointer) }
    }
}

可以看出,只有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方法,定义如下:

1
2
3
pub fn pin(x: T) -> Pin<Box<T>> {
    (box x).into()
}

例如,自定义了Node结构,如下的代码生成pin实例:

1
2
let node_pined: Pin<Box<Node>> = Box::pin(Node::new());
let movded_node_pined = node_pined;

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_lenVec::resize方法等,这些可都是safe等方法。这些方法的共同点都是需要&mut Self,所以说只要不暴露&mut Self,就可以达到pin的目标。

为什么需要pin?

事情的起因就是Async/.Await异步编程的需要。

看看如下异步编程的代码:

1
2
3
4
5
6
7
8
9
let fut_one = /* ... */;
let fut_two = /* ... */;
async move {
    ...
    fut_one.await;
    ...
    fut_two.await;
    ...
}

rustc在编译是会自动生成类似如下的代码,其中的AsyncFuture会是一个自引用结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// The `Future` type generated by our `async { ... }` block
struct AsyncFuture {
    ...
    fut_one: FutOne,
    fut_two: FutTwo,
    state: State,
}

// List of states our `async` block can be in
enum State {
    AwaitingFutOne,
    AwaitingFutTwo,
    Done,
}

impl Future for AsyncFuture {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        ...
    }
}

注意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,如下:

1
2
3
4
5
pub trait Future {
    type Item;
    type Error;
    fn poll(&mut self) -> Poll<Self::Item, Self::Error>;
}

总结

  • Pin实际是对P指针的限制,在T没有实现Unpin的情况下,避免P指针暴露&mut Self

  • Pin的引入是Async/.Await异步编程的需要,核心就是Future::poll方法参数的需要。

  • 除了Future::poll方法之外,不建议使用Pin,也没有必要使用Pin.

参考

  1. https://course.rs/advance/async/pin-unpin.html
  2. https://rustmagazine.github.io/rust_magazine_2021/chapter_12/rust-pin-advanced.html
  3. https://blog.cloudflare.com/pin-and-unpin-in-rust/
  4. https://course.rs/advance/circle-self-ref/self-referential.html