【译文】Rust异步编程: Pinning

pinned-tweet

原文:选自《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

让我们看看如果将test1test2交换导致数据移动会发生什么:

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

下图可以帮助您直观地了解正在发生的事情:

image

这很容易使它展现出未定义的行为并“壮观地”失败。

Pinning实践

让我们看下Pinning和Pin类型如何帮助我们解决此问题。

Pin类型封装了指针类型,它保证不会移动指针后面的值。例如,Pin<&mut T>Pin<&T>Pin<Box<T>>都保证T不被移动,当且仅当T:!Unpin

大多数类型在移动时都没有问题。这些类型实现了Unpin特型。可以将Unpin类型的指针自由的放置到Pin中或从中取出。例如,u8Unpin,因此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](https://docs.rs/pin-utils/0.1.0/pin_utils/)之类的板条箱来避免在固定到栈时编写我们自己的不安全代码。 下面,我们将对象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()));
}

如果现在尝试移动数据,则会出现编译错误:

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。对于没有UnpinFutureStream,您首先必须使用Box::pin(用于创建Pin<Box<T>>)或pin_utils::pin_mut!宏(用于创建Pin<&mut T>)来固定该值。 Pin<Box<Fut>>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

总结

  1. 如果是T:Unpin(这是默认设置),则Pin <'a, T>完全等于&'a mut T。换句话说:Unpin表示即使固定了此类型也可以移动,因此Pin将对这种类型没有影响。
  2. 如果是T:!Unpin,获得已固定T的&mut T需要unsafe。
  3. 大多数标准库类型都实现了Unpin。对于您在Rust中遇到的大多数“常规”类型也是如此。由async / await生成的Future是此规则的例外。
  4. 您可以在nightly使用功能标记添加!Unpin绑定到一个类型上,或者通过在stable将std::marker::PhantomPinned添加到您的类型上。
  5. 您可以将数据固定到栈或堆上。
  6. !Unpin对象固定到栈上需要unsafe
  7. !Unpin对象固定到堆并不需要unsafe。使用Box::pin可以执行此操作。
  8. 对于T:!Unpin的固定数据,您必须保持其不可变,即从固定到调用drop为止,其内存都不会失效或重新利用。这是pin约束的重要组成部分。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342