前言#
昨天我们了解到了async/await的一些细节
今天咱继续往下学
Pinning#
我们之前在给struct实现Future这个trait的时候,在poll方法中future也就是Self需要用到Pin这个类型包裹,当时并没有说这是个啥玩意儿。
这玩意儿是强制要求的,如果不使用编译就会直接报错。
那么它到底有啥用呢?
为啥要Pinning#
Pin和Unpin是串联使用的。Pinning让这个future可以保证实现了!Unpin的对象不会被moved。
我们先来看下一个例子
let fut_one = /* ... */;
let fut_two = /* ... */;
async move {
fut_one.await;
fut_two.await;
}它实际上会变成这样
// The `Future` type generated by our `async { ... }` block
struct AsyncFuture {
fut_one: FutOne,
fut_two: FutTwo,
state: State,
}
// List of states our `async` block can be in
enum State {
AwaitingFutOne,
AwaitingFutTwo,
Done,
}
impl Future for AsyncFuture {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
loop {
match self.state {
State::AwaitingFutOne => match self.fut_one.poll(..) {
Poll::Ready(()) => self.state = State::AwaitingFutTwo,
Poll::Pending => return Poll::Pending,
}
State::AwaitingFutTwo => match self.fut_two.poll(..) {
Poll::Ready(()) => self.state = State::Done,
Poll::Pending => return Poll::Pending,
}
State::Done => return Poll::Ready(()),
}
}
}
}把这俩异步任务放到一起,给他俩所在struct实现Future,然后等他俩都ready了才结束。
但如果这里异步中使用的是一个引用变量就不同了。
我们来看下例子
async {
let mut x = [0; 128];
let read_into_buf_fut = read_into_buf(&mut x);
read_into_buf_fut.await;
println!("{:?}", x);
}会变成这样
struct ReadIntoBuf<'a> {
buf: &'a mut [u8], // points to `x` below
}
struct AsyncFuture {
x: [u8; 128],
read_into_buf_fut: ReadIntoBuf<'what_lifetime?>,
}会把参数的生命周期也存储到future当中,这就形成了一个自引用(self-referential)类型。
但是,一旦这个future被移动到其它地方,这个存储的生命周期就会有问题了。
这个时候就轮到Pin出场了,只要pin了,就不能动了,自然就不会被移动了。
自引用类型的问题#
上面的这个问题最终可以总结为:如果在自引用(self-referential)类型中如何处理引用。
我们再来看个例子
#[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 {
assert!(!self.b.is_null(), "Test::b called without Test::init being called first");
unsafe { &*(self.b) }
}
}这个Test有俩字段,一个是a字符串,数据来自于new关联函数的参数txt,另一个是这个字符串的原始指针, new的时候是一个空指针,在init的时候变成a字段的指针。
a和b俩方法返回的数据理论上是一样的(b的错误场景不考虑)。
这里只能是原始指针,如果是引用指针就需要声明生命周期才行,但是这种自引用类型是没有办法的。
那么这就是一个自引用类型的例子。
我们来调用下
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());
}
确实是一样的。
然后我们再来修改下调用的代码
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());
}这里调用了std::mem::swap这个方法将这俩互相替换

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

为什么第二次打印的b不是test1呢?相信大家也都清楚了。
这个时候它的原始指针b并没有再次init,所以还是指向旧的test2.a地址。我们再来改下
println!("a: {}, b: {}", test1.a(), test1.b());
std::mem::swap(&mut test1, &mut test2);
test2.init();
println!("a: {}, b: {}", test2.a(), test2.b()); 
当我们再次init之后就正常了。
那么在没有再次init的情况下,这个自引用类型就错了,它的b不再是指向自己的a。
这个时候b的生命周期就不再可控了。
直接把官方的图拿过来了

说了这么多,该轮到Pin出场了
使用Pinning#
我们来看下Pin是如何帮助我们解决上面自引用类型问题的。
首先我们用Pin这个类型包裹住指针类型,这样就保证了这个指针指向的内存空间的数据不会被移走(前提是这个类型没有实现Unpin,注意不是!Unpin,他俩是相反的意思),比如Pin<&mut T>、Pin<&T>、Pin>。
大多数类型都不会有被移走的问题。而这些类型都实现了Unpin这个trait,比如u8类型就是实现了Unpin的。
而当Pin和Unpin都用在同一个类型的时候,按Unpin来处理。比如Pin<&mut u8>和&mut u8没差。
那么在异步当中,我们用Pin包裹self也就是future就是为了防止异步函数变成自引用类型之后数据被移动导致引用不准的问题。
我们来改下之前的代码
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(self: Pin<&mut Self>) {
let self_ptr: *const String = &self.a;
let this = unsafe { self.get_unchecked_mut() };
this.b = self_ptr;
}
fn a(self: Pin<&Self>) -> &str {
&self.get_ref().a
}
fn b(self: Pin<&Self>) -> &String {
assert!(!self.b.is_null(), "Test::b called without Test::init being called first");
unsafe { &*(self.b) }
}
}_marker: PhantomPinned用来标记这个类型是个非Unpin类型。get_unchecked_mut用来获取Pin包裹的对象,是可修改的。
然后我们再来调用下
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()));
}这里Pin::new_unchecked用了unsafe包裹, 如果涉及到生命周期,那就有可能超出控制。

扯远了,回到我们的代码中,当我们再次调用swap的时候会直接报错,因为此时test1/2都被pin了,不能从它的内存中将它抽走。
上面这个例子是pin到stack也就是栈内存里的。pin到栈内存里的就一定是unsafe的,需要用unsafe包裹
我们再来看个堆内存的例子
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.a;
unsafe { boxed.as_mut().get_unchecked_mut().b = self_ptr };
boxed
}
fn a(self: Pin<&Self>) -> &str {
&self.get_ref().a
}
fn b(self: Pin<&Self>) -> &String {
unsafe { &*(self.b) }
}
}
pub fn main() {
let test1 = Test::new("test1");
let 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());
}关键方法是Box::pin。
至于as_ref,我们来看下

就是包裹的new_unchecked方法,可以返回Pin包裹的对象,返回的是指针。
as_mut同as_ref,但是是可变的。
遇到Unpin#
说了这么多的Pin,如果我们遇到了需要Unpin的场景该如何做呢?比如一个Future自身不是Unpin的,但是它的方法里面需要Unpin的类型。
这种时候就需要通过Box::pin或者pin_utils::pin_mut!这个宏来创建可变的Pin,比如Pin<&mut T>。
Pin>或者Pin<&mut Future>都能用做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。换句话说,就是霸道啊~ - 如果
T: !Unpin,也就是Pin专场,这个时候如果可变的这个T的引用,就需要unsafe包裹。 - 绝大多数标准库里的类型都实现了
Unpin,当然不仅仅是标准库里的,rust自身的也一堆。使用async/await创建的Future是个例外。 - 可以用
!Unpin来约束某个类型,或者加上std::marker::PhantomPinned来保证稳定性。 - 将数据
pin在栈/堆内存里都是可以的。 - 将一个
!Unpin对象pin到栈内存中需要使用unsafe包裹。 - 将一个
!Unpin对象pin到堆内存里不需要unsafe包裹。一般都使用Box::pin。 pin``T:!Unpin的数据需要确保它依赖的数据式有效的并且生命周期至少比当前数据的作用域长,这是非常重要的一点。
参考#
- ^Pinning https://rust-lang.github.io/async-book/04_pinning/01_chapter.html#pinning
- ^why-pinning https://rust-lang.github.io/async-book/04_pinning/01_chapter.html#why-pinning
- ^Pinning-in-detail https://rust-lang.github.io/async-book/04_pinning/01_chapter.html#pinning-in-detail
- ^Pinning-in-practice https://rust-lang.github.io/async-book/04_pinning/01_chapter.html#pinning-in-practice
- ^summary https://rust-lang.github.io/async-book/04_pinning/01_chapter.html#summary
发布于 2023-01-30 16:19・IP 属地广东
