【譯文】Rust異步編程: Pinning

pinned-tweet

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

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

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

如果我們不移動任何數(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ù)移動會發(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è)指向不同對象字段的指針。這意味著我們不能再依賴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類型封裝了指針類型青扔,它保證不會移動指針后面的值。例如翩伪,Pin<&mut T>微猖,Pin<&T>Pin<Box<T>>都保證T不被移動缘屹,當(dāng)且僅當(dāng)T:!Unpin凛剥。

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

但是胳挎,固定后無法移動的類型具有一個(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,則將對象固定到棧始終是不安全的磅甩。您可以使用諸如[pin_utils](https://docs.rs/pin-utils/0.1.0/pin_utils/)之類的板條箱來避免在固定到棧時(shí)編寫我們自己的不安全代碼炊林。 下面,我們將對象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)在嘗試移動數(shù)據(jù)卷要,則會出現(xiàn)編譯錯誤:

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)阻止我們移動數(shù)據(jù)渣聚。

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

容易犯的一個(gè)錯誤就是忘記隱藏原始變量,因?yàn)槟梢詃ropPin并移動&'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ù)在固定后將無法移動。與棧固定相反宛蚓,我們知道數(shù)據(jù)將在對象的生命周期內(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。對于沒有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表示即使固定了此類型也可以移動盖喷,因此Pin將對這種類型沒有影響爆办。
  2. 如果是T:!Unpin,獲得已固定T的&mut T需要unsafe课梳。
  3. 大多數(shù)標(biāo)準(zhǔn)庫類型都實(shí)現(xiàn)了Unpin距辆。對于您在Rust中遇到的大多數(shù)“常規(guī)”類型也是如此。由async / await生成的Future是此規(guī)則的例外暮刃。
  4. 您可以在nightly使用功能標(biāo)記添加!Unpin綁定到一個(gè)類型上跨算,或者通過在stable將std::marker::PhantomPinned添加到您的類型上。
  5. 您可以將數(shù)據(jù)固定到椡职茫或堆上诸蚕。
  6. !Unpin對象固定到棧上需要unsafe
  7. !Unpin對象固定到堆并不需要unsafe。使用Box::pin可以執(zhí)行此操作背犯。
  8. 對于T:!Unpin的固定數(shù)據(jù)坏瘩,您必須保持其不可變,即從固定到調(diào)用drop為止漠魏,其內(nèi)存都不會失效或重新利用倔矾。這是pin約束的重要組成部分。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末柱锹,一起剝皮案震驚了整個(gè)濱河市哪自,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奕纫,老刑警劉巖提陶,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烫沙,死亡現(xiàn)場離奇詭異匹层,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)锌蓄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門升筏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瘸爽,你說我怎么就攤上這事您访。” “怎么了剪决?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵灵汪,是天一觀的道長。 經(jīng)常有香客問我柑潦,道長享言,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任渗鬼,我火速辦了婚禮览露,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘譬胎。我一直安慰自己差牛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布堰乔。 她就那樣靜靜地躺著偏化,像睡著了一般。 火紅的嫁衣襯著肌膚如雪镐侯。 梳的紋絲不亂的頭發(fā)上侦讨,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼搭伤。 笑死只怎,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的怜俐。 我是一名探鬼主播身堡,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拍鲤!你這毒婦竟也來了贴谎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤季稳,失蹤者是張志新(化名)和其女友劉穎擅这,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體景鼠,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡仲翎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了铛漓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溯香。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖浓恶,靈堂內(nèi)的尸體忽然破棺而出玫坛,到底是詐尸還是另有隱情,我是刑警寧澤包晰,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布湿镀,位于F島的核電站,受9級特大地震影響伐憾,放射性物質(zhì)發(fā)生泄漏勉痴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一塞耕、第九天 我趴在偏房一處隱蔽的房頂上張望蚀腿。 院中可真熱鬧,春花似錦扫外、人聲如沸莉钙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽磁玉。三九已至,卻和暖如春驾讲,著一層夾襖步出監(jiān)牢的瞬間蚊伞,已是汗流浹背席赂。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留时迫,地道東北人颅停。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像掠拳,于是被迫代替她去往敵國和親癞揉。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354