为什么需要 Pin
?
引入 Pin
的目的主要是为了支持 自引用类型 (self-referential types) 。下面我们以
Future
为例,解释一下自引用类型以及引入 Pin
的必要性。
由于异步块/异步函数中可能包含对局部变量的引用,例如下面的代码:
|
|
这类代码在生成 Future 结构时,就会出现 自引用类型 (self-referential types) 。例如,上面的代码可能生成类似下面的 Future 结构:
|
|
这类结构如果不能保证 self 地址的稳定性,则会出现严重的安全隐患。例如,如果 AsyncFuture 发生了移动,则 x 的地址也会发生变化,从而导致 ReadIntoBuf 中存储的 buf 指针失效。
要防止该问题,我们需要引入 Pin
来确保 self 地址的稳定性,以便在 async
块中安全地创建引用。
更多细节可参考 Pinning - Asynchronous Programming in Rust。
Pin
是什么?
Pin<P>
是一个struct
, 可用于包装任意的指针类型P
, 而Unpin
和!Unpin
都是trait
。Pin<P>
是一个智能指针,可保证其包装的指针P
后面的值不会发生移动(即物理地址保持稳定且有效),前提是其目标类型没有实现Unpin
。例如,
Pin<&mut T>
,Pin<&T>
,Pin<Box<T>>
都能保证T
不会发生移动,如果T: !Unpin
。大多数类型在移动时都没啥问题,这些类型实现了一个叫
Unpin
的 trait。对于这些类型来讲,Pin 前和 Pin 后在使用上基本一样,不受影响。例如
Pin<&mut u8>
的行为和&mut u8
没啥区别。由于标准库为
Unpin
提供了DerefMut
的一揽子实现,因此,如果T
实现了Unpin
trait, 则可以通过 Deref Coercion 自动获得Pin
对象的可变引用&mut T
, 也可以通过Pin::get_mut
方法手动获取&mut T
, 这意味着可以安全地对T
进行修改。详情可参考 Pin 的不可移动性是通过编译器来保证的 。
如果 T 带有
!Unpin
marker,一旦被 pin 则T
不可移动。Future
就是一个!Unpin
的例子。- 大多数类型默认实现了
Unpin
, 如果要强制实现!Unpin
, 可以在结构体中添加一个PhantomPinned
字段。参考!Unpin
和Pin<Box<T>>
示例 。
在
T: !Unpin
的前提下,保证T
不会发生移动意味着T
的物理地址是稳定且有效的,我们可以始终依赖该地址。这点主要通过限制对
T
的修改来解决。因为对于T: !Unpin
来说,我们拿不到Pin
所指向的T
的可变引用。详情可参考 Pin 的不可移动性是通过编译器来保证的 。🌟 对
Pin::set
方法的理解。Pin::set
方法可以设置一个新的T
值以替换旧值。请注意,这种替换永远是一个完整的、合法的T
值替换另一个T
值,这点和直接获取mut
引用有所不同,因为直接获取mut
引用可能会导致不安全的修改(例如对自引用类型的swap
操作)。同时,set
方法会引起旧的值的析构,因此是安全的,没有违反Pin
协议。Pin 可以发生在栈上,也可以发生在堆上。
栈上的 Pin 依赖于
unsafe
代码,且需要由 我们自己提供被 Pin 值的生命周期的保证,否则可能违反 Pin 契约。(更新:Rust 1.68 引入了 安全版本的栈上 pin 宏std::pin::pin!()
)。参考!Unpin
和Pin<Box<T>>
示例 。堆上的 Pin 直接用
Box::pin()
即可。参考Unpin
和Pin<Box<T>>
示例 。
Pin
的不可移动性是通过编译器来保证的
Pin<P>
仅为 Target 为 Unpin
的可变引用实现了 DerefMut
, 其他情况都只能获得 Target
的不可变引用。
参考标准库中的 Pin
实现:
|
|
Pin
的使用场景示例
需要通过 Future
的 &mut _
引用调用 .await
方法时
async fn
返回的 Future 是!Unpin
的- Future 在被 poll 之前,必须被 pin 住
- 直接在 Future 上调用
.await
会自动处理 pin 的逻辑,但是会将该 Future 消耗掉 - 要想不消耗掉 Future (例如在 loop 里面对 Future 进行
select!
), 需要通过 mut 引用来调用.await
方法。 - 通过 mut 引用
.await
前,需要我们自己手动先将 Future pin 住。 - Pin 有两种方式:
- 在堆上 pin: 使用
Box::pin
将数据分配到堆上并 pin 住。 - 在栈上 pin: 使用
tokio::pin!
(或者std::pin::pin!
) 宏,将数据 pin 在栈上。
- 在堆上 pin: 使用
参考 tokio::pin。
示例:
|
|
对 stream!
/ try_stream!
宏生成的 Stream
进行迭代时
和 Future
类似,由于 stream!
/ try_stream!
生成的 Stream 同样是 !Unpin
的,因此,在对其进行迭代操作前,同样需要先 pin 住。
详细信息可参考 Streams in Tokio。
!Unpin
和 Pin<Box<T>>
示例
|
|
|
|
Unpin
和 Pin<Box<T>>
示例
|
|
|
|