【譯文】Rust異步編程: Pinning

pinned-tweet

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

譯者注:如果你一時(shí)半會(huì)沒啃動(dòng)Pinning,也別心急,試試閱讀這篇《Rust的Pin與Unpin - Folyd》,理解起來會(huì)容易不少。

Pinning詳解

讓我們嘗試使用一個(gè)比較簡單的示例來了解pinning。前面我們遇到的問題,最終可以歸結(jié)為如何在Rust中處理自引用類型的引用的問題。

現(xiàn)在,我們的示例如下所示:

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是對(duì)a的引用,因此我們將其存儲(chǔ)為指針,因?yàn)镽ust的借用規(guī)則不允許我們定義這種生命周期。現(xiàn)在,我們有了所謂的自引用結(jié)構(gòu)。

如果我們不移動(dòng)任何數(shù)據(jù),則該示例運(yùn)行良好,可以通過運(yùn)行示例觀察:

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());
?
}

我們得到了我們期望的結(jié)果:

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

讓我們看看如果將test1test2交換導(dǎo)致數(shù)據(jù)移動(dòng)會(huì)發(fā)生什么:

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());
?
}

我們天真的以為應(yīng)該兩次獲得test1的調(diào)試打印,如下所示:

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

但我們得到的是:

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

test2.b的指針仍然指向了原來的位置,也就是現(xiàn)在的test1的里面。該結(jié)構(gòu)不再是自引用的,它擁有一個(gè)指向不同對(duì)象字段的指針。這意味著我們不能再依賴test2.b的生命周期和test2的生命周期的綁定假設(shè)了。

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

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());
?
}

下圖可以幫助您直觀地了解正在發(fā)生的事情:

image

這很容易使它展現(xiàn)出未定義的行為并“壯觀地”失敗。

Pinning實(shí)踐

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

Pin類型封裝了指針類型,它保證不會(huì)移動(dòng)指針后面的值。例如,Pin<&mut T>Pin<&T>,Pin<Box<T>>都保證T不被移動(dòng),當(dāng)且僅當(dāng)T:!Unpin

大多數(shù)類型在移動(dòng)時(shí)都沒有問題。這些類型實(shí)現(xiàn)了Unpin特型。可以將Unpin類型的指針自由的放置到Pin中或從中取出。例如,u8Unpin,因此Pin<&mut u8>的行為就像普通的&mut u8。

但是,固定后無法移動(dòng)的類型具有一個(gè)標(biāo)記為!Unpin的標(biāo)記。由async / await創(chuàng)建的Futures就是一個(gè)例子。

棧上固定

回到我們的例子。我們可以使用Pin來解決我們的問題。讓我們看一下我們的示例的樣子,我們需要一個(gè)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) }
    }
}

如果我們的類型實(shí)現(xiàn)!Unpin,則將對(duì)象固定到棧始終是不安全的。您可以使用諸如[pin_utils](https://docs.rs/pin-utils/0.1.0/pin_utils/)之類的板條箱來避免在固定到棧時(shí)編寫我們自己的不安全代碼。 下面,我們將對(duì)象test1test2固定到棧上:

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()));
}

如果現(xiàn)在嘗試移動(dòng)數(shù)據(jù),則會(huì)出現(xiàn)編譯錯(cuò)誤:

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()));
}

類型系統(tǒng)阻止我們移動(dòng)數(shù)據(jù)。

需要注意,棧固定將始終依賴于您在編寫unsafe時(shí)提供的保證。雖然我們知道&'a mut T所指的對(duì)象在生命周期'a中固定,但我們不知道'a結(jié)束后數(shù)據(jù)&'a mut T指向的數(shù)據(jù)是不是沒有移動(dòng)。如果移動(dòng)了,就違反了Pin約束。

容易犯的一個(gè)錯(cuò)誤就是忘記隱藏原始變量,因?yàn)槟梢詃ropPin并移動(dòng)&'a mut T背后的數(shù)據(jù),如下所示(這違反了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類型固定到堆將為我們的數(shù)據(jù)提供穩(wěn)定的地址,所以我們知道指向的數(shù)據(jù)在固定后將無法移動(dòng)。與棧固定相反,我們知道數(shù)據(jù)將在對(duì)象的生命周期內(nèi)固定。

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());
}

有的函數(shù)要求與之配合使用的futures是Unpin。對(duì)于沒有UnpinFutureStream,您首先必須使用Box::pin(用于創(chuàng)建Pin<Box<T>>)或pin_utils::pin_mut!宏(用于創(chuàng)建Pin<&mut T>)來固定該值。 Pin<Box<Fut>>Pin<&mut Fut>都可以作為futures使用,并且都實(shí)現(xiàn)了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

總結(jié)

  1. 如果是T:Unpin(這是默認(rèn)設(shè)置),則Pin <'a, T>完全等于&'a mut T。換句話說:Unpin表示即使固定了此類型也可以移動(dòng),因此Pin將對(duì)這種類型沒有影響。
  2. 如果是T:!Unpin,獲得已固定T的&mut T需要unsafe。
  3. 大多數(shù)標(biāo)準(zhǔn)庫類型都實(shí)現(xiàn)了Unpin。對(duì)于您在Rust中遇到的大多數(shù)“常規(guī)”類型也是如此。由async / await生成的Future是此規(guī)則的例外。
  4. 您可以在nightly使用功能標(biāo)記添加!Unpin綁定到一個(gè)類型上,或者通過在stable將std::marker::PhantomPinned添加到您的類型上。
  5. 您可以將數(shù)據(jù)固定到?;蚨焉?。
  6. !Unpin對(duì)象固定到棧上需要unsafe。
  7. !Unpin對(duì)象固定到堆并不需要unsafe。使用Box::pin可以執(zhí)行此操作。
  8. 對(duì)于T:!Unpin的固定數(shù)據(jù),您必須保持其不可變,即從固定到調(diào)用drop為止,其內(nèi)存都不會(huì)失效或重新利用。這是pin約束的重要組成部分。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容