坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.4

前言

昨天我们了解到了async/await的一些细节

坏蛋Dan:rust基础学习--异步day3

今天咱继续往下学


Pinning

我们之前在给struct实现Future这个trait的时候,在poll方法中future也就是Self需要用到Pin这个类型包裹,当时并没有说这是个啥玩意儿。

这玩意儿是强制要求的,如果不使用编译就会直接报错。

那么它到底有啥用呢?

为啥要Pinning

PinUnpin是串联使用的。Pinning让这个future可以保证实现了!Unpin的对象不会被moved

我们先来看下一个例子

它实际上会变成这样

把这俩异步任务放到一起,给他俩所在struct实现Future,然后等他俩都ready了才结束。

但如果这里异步中使用的是一个引用变量就不同了。

我们来看下例子

会变成这样

会把参数的生命周期也存储到future当中,这就形成了一个自引用(self-referential)类型。

但是,一旦这个future被移动到其它地方,这个存储的生命周期就会有问题了。

这个时候就轮到Pin出场了,只要pin了,就不能动了,自然就不会被移动了。


自引用类型的问题

上面的这个问题最终可以总结为:如果在自引用(self-referential)类型中如何处理引用。

我们再来看个例子

这个Test有俩字段,一个是a字符串,数据来自于new关联函数的参数txt,另一个是这个字符串的原始指针, new的时候是一个空指针,在init的时候变成a字段的指针。

ab俩方法返回的数据理论上是一样的(b的错误场景不考虑)。

这里只能是原始指针,如果是引用指针就需要声明生命周期才行,但是这种自引用类型是没有办法的。

那么这就是一个自引用类型的例子。

我们来调用下

确实是一样的。

然后我们再来修改下调用的代码

这里调用了std::mem::swap这个方法将这俩互相替换

那么这个时候理论上swap前后两次打印的结果应该是一样的。

但是实际结果并不是

为什么第二次打印的b不是test1呢?相信大家也都清楚了。

这个时候它的原始指针b并没有再次init,所以还是指向旧的test2.a地址。我们再来改下

当我们再次init之后就正常了。

那么在没有再次init的情况下,这个自引用类型就错了,它的b不再是指向自己的a

这个时候b的生命周期就不再可控了。

直接把官方的图拿过来了

说了这么多,该轮到Pin出场了


使用Pinning

我们来看下Pin是如何帮助我们解决上面自引用类型问题的。

首先我们用Pin这个类型包裹住指针类型,这样就保证了这个指针指向的内存空间的数据不会被移走(前提是这个类型没有实现Unpin,注意不是!Unpin,他俩是相反的意思),比如Pin<&mut T>、Pin<&T>、Pin>

大多数类型都不会有被移走的问题。而这些类型都实现了Unpin这个trait,比如u8类型就是实现了Unpin的。

而当PinUnpin都用在同一个类型的时候,按Unpin来处理。比如Pin<&mut u8>&mut u8没差。

那么在异步当中,我们用Pin包裹self也就是future就是为了防止异步函数变成自引用类型之后数据被移动导致引用不准的问题。

我们来改下之前的代码

  • _marker: PhantomPinned用来标记这个类型是个非Unpin类型。
  • get_unchecked_mut用来获取Pin包裹的对象,是可修改的。

然后我们再来调用下

这里Pin::new_unchecked用了unsafe包裹, 如果涉及到生命周期,那就有可能超出控制。

扯远了,回到我们的代码中,当我们再次调用swap的时候会直接报错,因为此时test1/2都被pin了,不能从它的内存中将它抽走。

上面这个例子是pinstack也就是栈内存里的。pin到栈内存里的就一定是unsafe的,需要用unsafe包裹

我们再来看个堆内存的例子

关键方法是Box::pin

至于as_ref,我们来看下

就是包裹的new_unchecked方法,可以返回Pin包裹的对象,返回的是指针。

as_mutas_ref,但是是可变的。


遇到Unpin

说了这么多的Pin,如果我们遇到了需要Unpin的场景该如何做呢?比如一个Future自身不是Unpin的,但是它的方法里面需要Unpin的类型。

这种时候就需要通过Box::pin或者pin_utils::pin_mut!这个宏来创建可变的Pin,比如Pin<&mut T>

Pin>或者Pin<&mut Future>都能用做futures,而它俩都实现了Unpin

来看下使用例子


总结

  1. 如果T: Unpin(默认),那么Pin<'a T>相当于&'a mut T。换句话说,就是霸道啊~
  2. 如果T: !Unpin,也就是Pin专场,这个时候如果可变的这个T的引用,就需要unsafe包裹。
  3. 绝大多数标准库里的类型都实现了Unpin,当然不仅仅是标准库里的,rust自身的也一堆。使用async/await创建的Future是个例外。
  4. 可以用!Unpin来约束某个类型,或者加上std::marker::PhantomPinned来保证稳定性。
  5. 将数据pin在栈/堆内存里都是可以的。
  6. 将一个!Unpin对象pin到栈内存中需要使用unsafe包裹。
  7. 将一个!Unpin对象pin到堆内存里不需要unsafe包裹。一般都使用Box::pin
  8. pin``T:!Unpin的数据需要确保它依赖的数据式有效的并且生命周期至少比当前数据的作用域长,这是非常重要的一点。

参考

  1. ^Pinning https://rust-lang.github.io/async-book/04_pinning/01_chapter.html#pinning
  2. ^why-pinning https://rust-lang.github.io/async-book/04_pinning/01_chapter.html#why-pinning
  3. ^Pinning-in-detail https://rust-lang.github.io/async-book/04_pinning/01_chapter.html#pinning-in-detail
  4. ^Pinning-in-practice https://rust-lang.github.io/async-book/04_pinning/01_chapter.html#pinning-in-practice
  5. ^summary https://rust-lang.github.io/async-book/04_pinning/01_chapter.html#summary

发布于 2023-01-30 16:19・IP 属地广东