轉: Rust中的Pin詳解 【Rust語言中文社區】

Rust中的Pin詳解
原創 automanyang Rust語言中文社區 昨天
https://mp.weixin.qq.com/s/PjctbPbyR5OeaqTHZdB5uQ
相關概念
Pin<P>
這是一個struct,作用就是將P所指向的T在內存中固定住,不能移動。說白一些,就是不能通過safe代碼拿到&mut T。
Pin

定義如下:

pub struct Pin<P> {
    pointer: P,
}

Unpin
這是一個trait,定義在std::marker中,如果一個T: Unpin,就說明T在pin後可以安全的移動,實際就是可以拿到&mut T。

pub auto trait Unpin {}

!Unpin
對Unpin取反,!Unpin的雙重否定就是pin。如果一個類型中包含了PhantomPinned,那麼這個類型就是!Unpin。

pub struct PhantomPinned;

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

Pin

的實現
我們這裏只關注safe方法,重點是new方法:

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就是應該被pin的實例,可是由於T: Unpin實際上T的實例並不會被pin。也就是說,T沒有實現Unpin trait時,T纔會被真正的pin住。
由於Pin::new方法要求T: Unpin,通常創建一個不支持Unpin的T的pin實例的方法是用Box::pin方法,定義如下:

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

例如,自定義了Node結構,如下的代碼生成pin實例:

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或&mut T)的結構,在T: Unpin的情況下,Pin<Box>和Box是類似的,通過DerefMut就可以直接得到&mut T,在T沒有實現Unpin的情況下,Pin<Box>只能通過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?
事情的起因就是Async/.Await異步編程的需要。
看看如下異步編程的代碼:

let fut_one = /* ... */;
let fut_two = /* ... */;
async move {
    ...
    fut_one.await;
    ...
    fut_two.await;
    ...
}

rustc在編譯是會自動生成類似如下的代碼,其中的AsyncFuture會是一個自引用結構:

// 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或者&mut Self,大概率是沒有問題的。很多executor的實現,都是要求Future是支持Unpin,因爲在poll代碼中的確有修改Self的需求,但不會產生錯誤,也是這個原因。
但是,對於程序員實現Future的情況,問題就來了。**如果poll的參數是&mut Self,那麼程序員就可能使用safe代碼(比如std::mem::swap)產生錯誤,這是與rust安全編碼的理念相沖突的。**這就是Pin引入的根本原因!
其實,在future 0.1版本中,poll的這個參數就是&mut Self,如下:

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.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章