袁承興:Rust異步編程 Pinning

【譯文】Rust異步編程: Pinning
袁承興

原文:選自《Rust異步編程》第4章 Pinning

譯者注:如果你一時半會沒啃動Pinning,也別心急,試試閱讀這篇《Rust的Pin與Unpin - Folyd》,理解起來會容易不少。
Pinning詳解
讓我們嘗試使用一個比較簡單的示例來了解pinning。前面我們遇到的問題,最終可以歸結爲如何在Rust中處理自引用類型的引用的問題。

現在,我們的示例如下所示:

use std::pin::Pin;#[derive(Debug)]
struct Test {
   
   
    a: String,
    b: *const String,
}
​
impl Test {
   
   
    fn new(txt: &str) -> Self {
   
   
        Test {
   
   
            a: String::from(txt),
            b: std::ptr::null(),
        }
    }
​
    fn init(&mut self) {
   
   
        let self_ref: *const String = &self.a;
        self.b = self_ref;
    }
​
    fn a(&self) -> &str {
   
   
        &self.a
    }
​
    fn b(&self) -> &String {
   
   
        unsafe {
   
   &*(self.b)}
    }
}

Test提供了獲取字段a和b值引用的方法。由於b是對a的引用,因此我們將其存儲爲指針,因爲Rust的借用規則不允許我們定義這種生命週期。現在,我們有了所謂的自引用結構。

如果我們不移動任何數據,則該示例運行良好,可以通過運行示例觀察:

fn main() {
   
   
    let mut test1 = Test::new("test1");
    test1.init();
    let mut test2 = Test::new("test2");
    test2.init();
​
    println!("a: {}, b: {}", test1.a(), test1.b());
    println!("a: {}, b: {}", test2.a(), test2.b());}

我們得到了我們期望的結果:

a: test1, b: test1
a: test2, b: test2

讓我們看看如果將test1與test2交換導致數據移動會發生什麼:

fn main() {
   
   
    let mut test1 = Test::new("test1");
    test1.init();
    let mut test2 = Test::new("test2");
    test2.init();
​
    println!("a: {}, b: {}", test1.a(), test1.b());
    std::mem::swap(&mut test1, &mut test2);
    println!("a: {}, b: {}", test2.a(), test2.b());}

我們天真的以爲應該兩次獲得test1的調試打印,如下所示:

a: test1, b: test1
a: test1, b: test1

但我們得到的是:

a: test1, b: test1
a: test1, b: test2

test2.b的指針仍然指向了原來的位置,也就是現在的test1的裏面。該結構不再是自引用的,它擁有一個指向不同對象字段的指針。這意味着我們不能再依賴test2.b的生命週期和test2的生命週期的綁定假設了。

如果您仍然不確定,那麼下面可以讓您確定了吧:

fn main() {
   
   
    let mut test1 = Test::new("test1");
    test1.init();
    let mut test2 = Test::new("test2");
    test2.init();
​
    println!("a: {}, b: {}", test1.a(), test1.b());
    std::mem::swap(&mut test1, &mut test2);
    test1.a = "I've totally changed now!".to_string();
    println!("a: {}, b: {}", test2.a(), test2.b());}

下圖可以幫助您直觀地瞭解正在發生的事情:

在這裏插入圖片描述

這很容易使它展現出未定義的行爲並“壯觀地”失敗。

Pinning實踐
讓我們看下Pinning和Pin類型如何幫助我們解決此問題。

Pin類型封裝了指針類型,它保證不會移動指針後面的值。例如,Pin<&mut T>,Pin<&T>,Pin<Box>都保證T不被移動,當且僅當T:!Unpin。

大多數類型在移動時都沒有問題。這些類型實現了Unpin特型。可以將Unpin類型的指針自由的放置到Pin中或從中取出。例如,u8是Unpin,因此Pin<&mut u8>的行爲就像普通的&mut u8。

但是,固定後無法移動的類型具有一個標記爲!Unpin的標記。由async / await創建的Futures就是一個例子。

棧上固定
回到我們的例子。我們可以使用Pin來解決我們的問題。讓我們看一下我們的示例的樣子,我們需要一個pinned的指針:

use std::pin::Pin;
use std::marker::PhantomPinned;#[derive(Debug)]
struct Test {
   
   
    a: String,
    b: *const String,
    _marker: PhantomPinned,
}
​
​
impl Test {
   
   
    fn new(txt: &str) -> Self {
   
   
        Test {
   
   
            a: String::from(txt),
            b: std::ptr::null(),
            _marker: PhantomPinned, // This makes our type `!Unpin`
        }
    }
    fn init<'a>(self: Pin<&'a mut Self>) {
   
   
        let self_ptr: *const String = &self.a;
        let this = unsafe {
   
    self.get_unchecked_mut() };
        this.b = self_ptr;
    }
​
    fn a<'a>(self: Pin<&'a Self>) -> &'a str {
        &self.get_ref().a
    }
​
    fn b<'a>(self: Pin<&'a Self>) -> &'a String {
   
   
        unsafe {
   
    &*(self.b) }
    }
}

如果我們的類型實現!Unpin,則將對象固定到棧始終是不安全的。您可以使用諸如pin_utils之類的板條箱來避免在固定到棧時編寫我們自己的不安全代碼。 下面,我們將對象test1和test2固定到棧上:

pub fn main() {
   
   
    // test1 is safe to move before we initialize it
    let mut test1 = Test::new("test1");
    // Notice how we shadow `test1` to prevent it from being accessed again
    let mut test1 = unsafe {
   
    Pin::new_unchecked(&mut test1) };
    Test::init(test1.as_mut());let mut test2 = Test::new("test2");
    let mut test2 = unsafe {
   
    Pin::new_unchecked(&mut test2) };
    Test::init(test2.as_mut());
​
    println!("a: {}, b: {}", Test::a(test1.as_ref()), Test::b(test1.as_ref()));
    println!("a: {}, b: {}", Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}

如果現在嘗試移動數據,則會出現編譯錯誤:

pub fn main() {
   
   
    let mut test1 = Test::new("test1");
    let mut test1 = unsafe {
   
    Pin::new_unchecked(&mut test1) };
    Test::init(test1.as_mut());let mut test2 = Test::new("test2");
    let mut test2 = unsafe {
   
    Pin::new_unchecked(&mut test2) };
    Test::init(test2.as_mut());
​
    println!("a: {}, b: {}", Test::a(test1.as_ref()), Test::b(test1.as_ref()));
    std::mem::swap(test1.get_mut(), test2.get_mut());
    println!("a: {}, b: {}", Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}

類型系統阻止我們移動數據。

需要注意,棧固定將始終依賴於您在編寫unsafe時提供的保證。雖然我們知道&'a mut T所指的對象在生命週期’a中固定,但我們不知道’a結束後數據&'a mut T指向的數據是不是沒有移動。如果移動了,就違反了Pin約束。

容易犯的一個錯誤就是忘記隱藏原始變量,因爲您可以dropPin並移動&'a mut T背後的數據,如下所示(這違反了Pin約束):

fn main() {
   
   
   let mut test1 = Test::new("test1");
   let mut test1_pin = unsafe {
   
    Pin::new_unchecked(&mut test1) };
   Test::init(test1_pin.as_mut());
   drop(test1_pin);
   println!(r#"test1.b points to "test1": {:?}..."#, test1.b);
   let mut test2 = Test::new("test2");
   mem::swap(&mut test1, &mut test2);
   println!("... and now it points nowhere: {:?}", test1.b);
}

堆上固定
將!Unpin類型固定到堆將爲我們的數據提供穩定的地址,所以我們知道指向的數據在固定後將無法移動。與棧固定相反,我們知道數據將在對象的生命週期內固定。

use std::pin::Pin;
use std::marker::PhantomPinned;#[derive(Debug)]
struct Test {
   
   
    a: String,
    b: *const String,
    _marker: PhantomPinned,
}
​
impl Test {
   
   
    fn new(txt: &str) -> Pin<Box<Self>> {
   
   
        let t = Test {
   
   
            a: String::from(txt),
            b: std::ptr::null(),
            _marker: PhantomPinned,
        };
        let mut boxed = Box::pin(t);
        let self_ptr: *const String = &boxed.as_ref().a;
        unsafe {
   
    boxed.as_mut().get_unchecked_mut().b = self_ptr };
​
        boxed
    }
​
    fn a<'a>(self: Pin<&'a Self>) -> &'a str {
        &self.get_ref().a
    }
​
    fn b<'a>(self: Pin<&'a Self>) -> &'a String {
   
   
        unsafe {
   
    &*(self.b) }
    }
}
​
pub fn main() {
   
   
    let mut test1 = Test::new("test1");
    let mut test2 = Test::new("test2");
​
    println!("a: {}, b: {}",test1.as_ref().a(), test1.as_ref().b());
    println!("a: {}, b: {}",test2.as_ref().a(), test2.as_ref().b());
}

有的函數要求與之配合使用的futures是Unpin。對於沒有Unpin的Future或Stream,您首先必須使用Box::pin(用於創建Pin<Box>)或pin_utils::pin_mut!宏(用於創建Pin<&mut T>)來固定該值。 Pin<Box>和Pin<&mut Fut>都可以作爲futures使用,並且都實現了Unpin。

例如:

use pin_utils::pin_mut; // `pin_utils` is a handy crate available on crates.io
​
// A function which takes a `Future` that implements `Unpin`.
fn execute_unpin_future(x: impl Future<Output = ()> + Unpin) {
   
    /* ... */ }let fut = async {
   
    /* ... */ };
execute_unpin_future(fut); // Error: `fut` does not implement `Unpin` trait
​
// Pinning with `Box`:
let fut = async {
   
    /* ... */ };
let fut = Box::pin(fut);
execute_unpin_future(fut); // OK
​
// Pinning with `pin_mut!`:
let fut = async {
   
    /* ... */ };
pin_mut!(fut);
execute_unpin_future(fut); // OK

總結
如果是T:Unpin(這是默認設置),則Pin <'a, T>完全等於&'a mut T。換句話說:Unpin表示即使固定了此類型也可以移動,因此Pin將對這種類型沒有影響。
如果是T:!Unpin,獲得已固定T的&mut T需要unsafe。
大多數標準庫類型都實現了Unpin。對於您在Rust中遇到的大多數“常規”類型也是如此。由async / await生成的Future是此規則的例外。
您可以在nightly使用功能標記添加!Unpin綁定到一個類型上,或者通過在stable將std::marker::PhantomPinned添加到您的類型上。
您可以將數據固定到棧或堆上。
將!Unpin對象固定到棧上需要unsafe。
將!Unpin對象固定到堆並不需要unsafe。使用Box::pin可以執行此操作。
對於T:!Unpin的固定數據,您必須保持其不可變,即從固定到調用drop爲止,其內存都不會失效或重新利用。這是pin約束的重要組成部分。
編輯於 01-03








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