昨天我们了解到了async/await
的一些细节
今天咱继续往下学
我们之前在给struct
实现Future
这个trait
的时候,在poll
方法中future
也就是Self
需要用到Pin
这个类型包裹,当时并没有说这是个啥玩意儿。
这玩意儿是强制要求的,如果不使用编译就会直接报错。
那么它到底有啥用呢?
Pin
和Unpin
是串联使用的。Pinning
让这个future
可以保证实现了!Unpin
的对象不会被moved
。
我们先来看下一个例子
它实际上会变成这样
把这俩异步任务放到一起,给他俩所在struct
实现Future
,然后等他俩都ready
了才结束。
但如果这里异步中使用的是一个引用变量就不同了。
我们来看下例子
会变成这样
会把参数的生命周期也存储到future
当中,这就形成了一个自引用(self-referential
)类型。
但是,一旦这个future
被移动到其它地方,这个存储的生命周期就会有问题了。
这个时候就轮到Pin
出场了,只要pin
了,就不能动了,自然就不会被移动了。
上面的这个问题最终可以总结为:如果在自引用(self-referential
)类型中如何处理引用。
我们再来看个例子
这个Test
有俩字段,一个是a
字符串,数据来自于new
关联函数的参数txt
,另一个是这个字符串的原始指针, new
的时候是一个空指针,在init
的时候变成a
字段的指针。
a
和b
俩方法返回的数据理论上是一样的(b
的错误场景不考虑)。
这里只能是原始指针,如果是引用指针就需要声明生命周期才行,但是这种自引用类型是没有办法的。
那么这就是一个自引用类型的例子。
我们来调用下
确实是一样的。
然后我们再来修改下调用的代码
这里调用了std::mem::swap
这个方法将这俩互相替换
那么这个时候理论上swap
前后两次打印的结果应该是一样的。
但是实际结果并不是
为什么第二次打印的b
不是test1
呢?相信大家也都清楚了。
这个时候它的原始指针b
并没有再次init
,所以还是指向旧的test2.a
地址。我们再来改下
当我们再次init
之后就正常了。
那么在没有再次init
的情况下,这个自引用类型就错了,它的b
不再是指向自己的a
。
这个时候b
的生命周期就不再可控了。
直接把官方的图拿过来了
说了这么多,该轮到Pin
出场了
我们来看下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
就是为了防止异步函数变成自引用类型之后数据被移动导致引用不准的问题。
我们来改下之前的代码
_marker: PhantomPinned
用来标记这个类型是个非Unpin
类型。get_unchecked_mut
用来获取Pin
包裹的对象,是可修改的。然后我们再来调用下
这里Pin::new_unchecked
用了unsafe
包裹, 如果涉及到生命周期,那就有可能超出控制。
扯远了,回到我们的代码中,当我们再次调用swap
的时候会直接报错,因为此时test1/2
都被pin
了,不能从它的内存中将它抽走。
上面这个例子是pin
到stack
也就是栈内存里的。pin
到栈内存里的就一定是unsafe
的,需要用unsafe
包裹
我们再来看个堆内存的例子
关键方法是Box::pin
。
至于as_ref
,我们来看下
就是包裹的new_unchecked
方法,可以返回Pin
包裹的对象,返回的是指针。
as_mut
同as_ref
,但是是可变的。
说了这么多的Pin
,如果我们遇到了需要Unpin
的场景该如何做呢?比如一个Future
自身不是Unpin
的,但是它的方法里面需要Unpin
的类型。
这种时候就需要通过Box::pin
或者pin_utils::pin_mut!
这个宏来创建可变的Pin
,比如Pin<&mut T>
。
Pin>
或者Pin<&mut Future>
都能用做futures
,而它俩都实现了Unpin
。
来看下使用例子
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
的数据需要确保它依赖的数据式有效的并且生命周期至少比当前数据的作用域长,这是非常重要的一点。发布于 2023-01-30 16:19・IP 属地广东