Rust 爲什麼需要 Pin、Unpin?

使用異步 Rust 庫通常很容易,這就像使用普通的 Rust 代碼一樣,使用. async 或. await。但是編寫自己的異步庫可能很困難。有一些晦澀難懂的語法,比如 T: Unpin 和 Pin<&mut Self>。因此,在這篇文章中,我們將解釋這些語法。

自引用是不安全的

Pin 的存在是爲了解決一個非常具體的問題: 自引用數據類型,即具有指向自身的指針的數據結構。例如,二叉搜索樹可能具有自引用指針,這些指針指向同一結構中的其他節點。

自引用類型可能非常有用,但它們很難保證內存安全。爲了瞭解原因,讓我們使用這個帶有兩個字段的類型作爲示例,一個名爲 val 的 i32 類型的字段和一個名爲 pointer 的指向 i32 的指針。

到目前爲止,一切正常。指針字段指向內存地址 A 中的 val 字段,其中包含一個有效的 i32。所有指針都是有效的,也就是說,它們指向的內存確實編碼了正確類型的值 (在本例中是 i32)。

但是 Rust 編譯器經常在內存中移動值。例如,如果將這個結構體傳遞給另一個函數,它可能會被移動到不同的內存地址,或者我們使用 Box 把它放在堆上;或者,如果這個結構體在 Vec 中,並且我們將更多的值壓入,Vec 可能會超出其容量,需要將其元素移動到一個新的更大的緩衝區中。

當我們移動它時,結構體的字段會改變它們的地址,但不會改變它們的值。所以指針字段仍然指向地址 A,但是地址 A 現在沒有一個有效的 i32。原來在那裏的數據被移到了地址 B,而其他一些值可能被寫到了那裏,現在指針是無效的。

這很糟糕——在最好的情況下,無效指針會導致崩潰,在最壞的情況下,它們會導致可攻擊的漏洞。我們應該非常小心地記錄這種類型,並告訴用戶在移動後更新指針。

Unpin 和 !Unpin

回顧一下,所有 Rust 類型都分爲兩類:

1,可以安全地在內存中移動的類型。這是默認的,是常態。例如,這包括像數字、字符串、bool 這樣的原語,以及完全由它們組成的結構體或枚舉。大多數類型都屬於這一類!

2,自引用類型,在內存中移動是不安全的。這是非常罕見的,一個例子是一些 Tokio 內部內部的侵入式鏈表,另一個例子是大多數實現 Future,同時也借用了數據的類型。

類別 1 中的類型在內存中移動是完全安全的,移動指針不會使它們失效。但是,如果在類別 2 中移動一個類型,那麼指針就會失效,並可能得到未定義的行爲,正如我們之前看到的那樣。在早期的 Rust 版本中,你必須非常小心地使用這些類型,不要移動它們,或者如果你移動了它們,使用不安全並更新所有的指針。但是從 Rust 1.33 開始,編譯器可以自動找出任何類型屬於哪個類別,並確保您只安全地使用它。

類別 1 中的任何類型都自動實現了一個稱爲 Unpin 的特殊 Trait。奇怪的名字,但它的意思很快就會清楚。同樣,大多數 “正常” 類型實現了 Unpin,因爲它是一個自動實現的 Trait(像 Send 或 Sync 或 Sized),所以你不必擔心自己實現它。如果你不確定是否可以安全地移動類型,只需在文檔中檢查它是否實現了 Unpin 即可。

類別 2 中的類型是創造性地命名爲! Unpin(! 在 trait 中意味着 “不實現”)。爲了安全地使用這些類型,不能使用常規指針進行自引用。相反,我們使用特殊的指針來“固定” 它們的值,確保它們不能被移動,這正是 Pin 類型所做的。

Pin 封裝指針並阻止其值移動,唯一的例外是如果值包含 Unpin,那麼我們就知道移動是安全的。現在我們可以安全地編寫自引用結構了!這一點非常重要,因爲正如上面所討論的,許多 future 都是自引用的,我們需要它們來實現 async/await。

使用 Pin

現在我們理解了 Pin 存在的原因,以及爲什麼我們的 Future poll 方法有一個固定的 & mut self 到 self,而不是一個常規的 & mut self。那麼讓我們回到之前的問題:我需要一個指向內部 Future 的固定引用。更一般地說:給定一個固定的結構體,我們如何訪問它的字段?

解決方案是編寫幫助函數,爲你提供對字段的引用。這些引用可能是普通的 Rust 引用,比如 & mut,或者它們也可能是固定的。你可以選擇你需要的任何一個。這就是所謂的投影:如果你有一個固定的結構體,你可以編寫一個投影方法,讓你訪問它的所有字段。

投影實際上就是數據與 Pin 類型互相轉換,例如,我們從 Pin<&mut self> 中獲得 start: Option 字段,並且我們需要將 future: Fut 放入 Pin 中,以便我們可以調用其 poll 方法)。如果你閱讀 Pin 方法,你會發現如果它指向一個 Unpin 值,它總是安全的,否則就要求使用 Unsafe。

// 將數據放入Pin
pub        fn new          <P: Deref<Target:Unpin>>(pointer: P) -> Pin<P>;
pub unsafe fn new_unchecked<P>                     (pointer: P) -> Pin<P>;

// 從Pin獲取數據
pub        fn into_inner          <P: Deref<Target: Unpin>>(pin: Pin<P>) -> P;
pub unsafe fn into_inner_unchecked<P>                      (pin: Pin<P>) -> P;

用 pin-project 庫

對於結構體中的每個字段,你必須選擇是否應該固定其引用。默認情況下,應該使用普通引用,因爲它們更容易、更簡單。但是如果你知道你需要一個固定的引用——例如,因爲你想調用. poll(),它的接收者是 Pin<&mut Self>——那麼你可以用 #[Pin] 來做。

例子如下:

在 Cargo.toml 文件中加入 pin-project 依賴項:

[dependencies]
pin-project = "1.1.3"

在 src/main.rs 中,寫入以下代碼:

#[pin_project::pin_project]
pub struct TimedWrapper<Fut: Future> {
    // 對於每個字段,我們需要選擇是返回對該字段的未固定(&mut)引用
    // 還是固定(Pin<&mut >)引用。
    // 默認情況下,它是未固定的
    start: Option<Instant>,
    // 此屬性選擇固定引用
    #[pin]
    future: Fut,
}

poll 方法實現如下:

impl<Fut: Future> Future for TimedWrapper<Fut> {
    type Output = (Fut::Output, Duration);

    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
        // 這將返回一個具有所有相同字段的類型,所有相同的類型,
        // 除了用#[pin]定義的字段將被固定。
        let mut this = self.project();

        // 調用內部poll,測量花了多長時間。
        let start = this.start.get_or_insert_with(Instant::now);
        let inner_poll = this.future.as_mut().poll(cx);
        let elapsed = start.elapsed();

        match inner_poll {
            // 內部Future需要更多的時間,所以這個Future也需要更多的時間
            Poll::Pending => Poll::Pending,
            // Success!
            Poll::Ready(output) => Poll::Ready((output, elapsed)),
        }
    }
}

最後,我們的目標完成了——我們在沒有任何不安全代碼的情況下完成了這一切。

總結

如果 Rust 類型具有自引用指針,則不能安全地移動它。畢竟,移動並沒有更新指針,所以它們仍然指向舊的內存地址,所以它們是無效的。

Rust 可以自動判斷哪些類型可以安全移動 (並將自動爲它們實現 Unpin trait)。如果你有一個 Pin 的指針指向某些數據,Rust 可以保證不會發生任何不安全的事情。這一點很重要,因爲許多 Future 類型都是自引用的,所以我們需要 Pin 來安全地輪詢 Future。你可以使用 pin-project crate 來簡化操作。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xeSvJz4TY2gBqqfg_n_IVQ