在「Rust学习笔记 - Future、Waker 与异步任务调度」中实现异步任务执行器 block_on 时,用到了 Pin 来保证某些值在内存中不被移动,想着展开学习一下。
什么是值的移动
Rust 中的值默认可以移动。移动通常发生在赋值、传参和从函数返回时,例如:
1 | let name = String::from("hallo"); |
这里不是把 String 中的文本复制了一份。name 持有的值被转移给 other,之后也不能再通过 name 访问它。对于 String 这类普通类型,移动通常不需要关心它在内存中的具体位置。只要所有权完成转移,内部管理的堆内存仍然可以正常释放和访问。
不过,Rust 并不保证值在移动前后位于同一个内存地址。看下面的代码:
1 | let value = String::from("hallo"); |
多数时候,内存地址变化不会造成问题。但有些值的内部状态依赖自身所在的内存地址,如果值在不合适的时机被移动,原本成立的约束就可能被破坏。
什么样的值不能随意移动
有些值会在内部保存指向自身字段的指针,这类值一旦建立了内部指针,之后就不能再随意移动。
例如,下面的 Line 除了保存文本,还想保存一个指向自身 text 字段的指针:
1 | struct Line { |
假设 Line 位于地址 A,text 字段位于这块内存中的某个位置。初始化后,text_ptr 保存的就是这个 text 字段的地址。
如果整个 Line 被移动到地址 B,text 字段也会随之移动到地址 B 对应的位置,但 text_ptr 仍然保存旧地址 A 中 text 的位置。此时,它不再指向当前 Line 中的 text,原本建立的内部指针就失效了。
Pin 如何限制移动
Pin 可以理解成一个带有额外约束的指针。它包住的不是值本身,而是一个指向值的指针:
1 | Pin<P> |
其中 P 是 &mut T、Box<T> 这类指针类型,Pin 要约束的是指针指向的值。
例如:
1 | use std::pin::Pin; |
被 Pin 固定的是 Box 指向的 String,而不是 Box 本身。Box 只是保存堆地址的指针,它仍然可以被移动:
1 | let pinned = Box::pin(String::from("hallo")); |
这里移动的是 Pin<Box<String>> 这个指针对象,other 仍然指向原来那块堆内存。
值得注意的是,String 实现了 Unpin,即使被包在 Pin<Box<String>> 中,也仍然可以安全移动。Pin 的限制主要针对 !Unpin 类型:Pin<&mut T> 不会直接提供普通的 &mut T,否则调用方就可能通过赋值或 std::mem::replace 将值移动出去。
Pin 限制的是安全代码的访问方式,不是在内存层面强行禁止移动。unsafe 代码仍然可以绕过限制,但调用方必须自行保证不移动已经固定的 !Unpin 值,否则依赖原地址的内部指针或引用可能失效,进而导致未定义行为。
Unpin 是什么
Unpin 是一个自动特型,大多数普通类型都实现了它,例如整型、String 和 Vec<T>。
实现 Unpin 表示这个类型的值即使被移动,也不会破坏类型自身的约束。当 T: Unpin 时,Pin<&mut T> 可以安全地转换回普通的 &mut T。相反,!Unpin 表示这个类型可能存在依赖内存地址的状态。对于已经固定的 !Unpin 值,安全代码不能再将它整体移动出去。
!Unpin 的示例:
1 | struct PinnedLine { |
line.as_mut() 可以得到 Pin<&mut PinnedLine>,因为这个操作不会移动 PinnedLine。但 pinned.get_mut() 需要 PinnedLine: Unpin。由于 PinnedLine 包含 PhantomPinned,它没有实现 Unpin,因此不能安全地取得普通的 &mut PinnedLine。
Future 中的 Pin
Future 特型中的 poll 方法,接收的不是普通的 &mut self,而是 Pin<&mut Self>:
1 | pub trait Future { |
这是因为某些 Future 在执行过程中可能会进入依赖自身内存地址的状态。比如 async fn 编译后会生成一个状态机,用来保存 .await 前后的局部变量和执行位置。当这个状态机第一次被 poll 后,后续的 poll 需要继续使用之前保存的状态。如果这些状态依赖 Future 自身的地址,那么 Future 就不能在两次 poll 之间被移动。
这也解释了上一篇笔记中 block_on 的这行代码:
1 | let mut future = Box::pin(future); |
Box::pin(future) 返回 Pin<Box<F>>。之后调用 future.as_mut(),可以得到 Future::poll 所需要的 Pin<&mut F>:
1 | future.as_mut().poll(&mut cx) |
这样,执行器在多次调用 poll 时,都会通过 Pin 访问同一个 Future,避免需要稳定地址的 Future 在两次 poll 之间被随意移动。
最后
Pin 的思想不是让所有值都不能移动,而是为需要稳定地址的 !Unpin 值提供约束。普通类型通常实现了 Unpin,即使被包在 Pin 中,也仍然可以安全地移动。
当然,也可以通过 unsafe 绕过 Pin 的限制,将被固定的值移动到新的内存位置:
1 | let mut line = Box::pin(PinnedLine { |
这段不安全的代码能够正常运行,但它已经不再受 Pin 的安全约束保护。