为什么需要 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实现了Unpintrait, 则可以通过 Deref Coercion 自动获得Pin对象的可变引用&mut T, 也可以通过Pin::get_mut方法手动获取&mut T, 这意味着可以安全地对T进行修改。详情可参考 Pin 的不可移动性是通过编译器来保证的 。
如果 T 带有
!Unpinmarker,一旦被 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>> 示例
| |
| |