【譯文】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