Skip to content

Day 26: Pin与Unpin

在学习async/await时,我们接触到了Future。但Future的实现涉及一个重要的概念:Pin。Pin和Unpin是Rust用来处理自引用结构(self-referential structures)的机制,理解它们对于深入掌握async编程至关重要。

考虑一个简单的数据结构,其中包含一个字段和该字段的某种”视图”:

struct SelfRef {
value: String,
// 指针指向value字段
pointer: *const String,
}
impl SelfRef {
fn new(value: String) -> Self {
SelfRef {
value,
pointer: std::ptr::null(),
}
}
fn set_pointer(&mut self) {
self.pointer = &self.value as *const String;
}
}

这段代码的问题在于:当SelfRef被移动到其他位置时,value的内存地址会变化,但pointer仍然指向旧的地址,导致悬空指针。

Rust用Unpin标记来区分可以在内存中安全移动的类型。大多数类型都是Unpin的:

fn main() {
// 大多数类型都实现了Unpin
let x: i32 = 5;
let y = x; // x被移动,但i32是Unpin,所以可以安全移动
println!("{}", y);
// 手动指定Unpin
struct MyStruct(u32);
impl Unpin for MyStruct {}
}

对于Unpin类型,编译器允许我们在持有 &mut Self 时安全地将值从内存的一个位置移动到另一个位置。

Pin是一个wrapper,它包装了一个类型T,并限制了对T的可变访问,从而防止T被移动(除非T: Unpin)。

use std::pin::Pin;
use std::marker::PhantomPinned;
fn main() {
// Pin<&mut T>的行为
let mut x = 5;
// Pin<&mut i32> - i32是Unpin,所以Pin<i32>等价于i32
let pinned: Pin<&mut i32> = Pin::new(&mut x);
println!("pinned value: {}", *pinned);
// PhantomPinned是不实现Unpin的类型
struct Test {
_pinned: PhantomPinned,
}
let mut test = Test { _pinned: PhantomPinned };
// let mut test_pinned = Pin::new(&mut test);
// 注释掉的代码无法工作,因为Test不实现Unpin
}

Pin有两种主要形式:不可变Pin和Box包装的Pin。

Pin是不可变引用,防止通过这个引用移动值:

use std::pin::Pin;
struct Test {
value: String,
}
impl Drop for Test {
fn drop(&mut self) {
println!("Test被释放");
}
}
fn take_ownership(_: Test) {}
fn main() {
let mut test = Test { value: String::from("hello") };
// 正常情况下,可以将test移动到函数中
// take_ownership(test);
// 但如果我们"pin"了它,就不能再移动
let pinned: Pin<&mut Test> = Pin::new(&mut test);
// pinned允许访问底层数据,但限制了移动
println!("value: {}", pinned.value);
// 无法获取&mut Test然后移动它
// let raw = pinned.get_mut(); // 这不是公开API
// take_ownership(raw); // 不可能这样移动
}

async/.await生成的Future经常包含自引用结构。考虑:

async fn async_process() {
let data = String::from("hello");
// async块中的Future可能需要捕获data的引用
// 但同时data和Future可能被移动
let _future = async {
// 这里可能会生成指向data的指针
println!("{}", data.len());
data
};
}

编译器生成的Future可能是自引用的。如果Future被移动,自引用就会失效。Pin通过固定(pinning)Future在内存位置来解决这个问题。

use std::pin::Pin;
fn main() {
// Pin::new要求T: Unpin
let mut x = 5;
let pinned = Pin::new(&mut x);
// 对于!Unpin类型,需要使用unsafe的Pin::new_unchecked
// let pinned = unsafe { Pin::new_unchecked(&mut some_pinned_struct) };
}

标准库提供了Pin::new_unchecked用于处理!Unpin类型,但这需要unsafe代码。通常我们使用box来创建pin:

use std::pin::Pin;
use std::marker::PhantomPinned;
#[tokio::main]
async fn main() {
// Box::pin创建一个固定在堆上的Future
let future = Box::pin(async {
println!("这是一个pinned的future");
});
// 可以在await前移动future
let moved_future = future;
moved_future.await;
}

Pin提供了一些方法来访问底层数据:

use std::pin::Pin;
fn main() {
let mut x = 5;
// 创建一个Pin
let mut pinned = Pin::new(&mut x);
// get_mut要求T: Unpin
if pinned.is_unpin() {
let deref = Pin::get_mut(pinned);
*deref = 10;
}
// as_mut允许获取可变引用进行一些操作
let as_mut = Pin::as_mut(&mut pinned);
*as_mut = 20;
println!("x = {}", x);
}

如果需要在自己的类型中支持Pin,通常需要实现Future或使用std::pin模块:

use std::pin::Pin;
use std::task::{Context, Poll};
struct MyFuture {
data: String,
index: usize,
}
impl std::future::Future for MyFuture {
type Output = char;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// self已经是被Pin包裹的
let this = self.as_mut().get_mut();
if this.index < this.data.len() {
let ch = this.data.chars().nth(this.index).unwrap();
this.index += 1;
Poll::Ready(ch)
} else {
Poll::Ready(' ')
}
}
}
fn main() {
let future = MyFuture {
data: String::from("hello"),
index: 0,
};
// 使用pin_project或手动处理Pin
let pinned = Box::pin(future);
// 驱动Future完成
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(pinned);
println!("第一个字符: {}", result);
}

今天我们深入理解了Pin和Unpin机制:

  1. 自引用结构是包含指向自身字段的指针的数据结构,移动时会导致悬空指针
  2. Unpin是一个标记trait,实现它的类型可以安全移动
  3. Pin通过限制对底层数据的访问,防止T被移动
  4. async/.await生成的Future经常是自引用的,需要Pin来固定
  5. Pin::new_unchecked和Box::pin是创建固定Future的常用方法

Pin是Rust类型系统处理自引用结构的精妙设计,它使得异步编程在类型安全的前提下成为可能。