从自引用类型谈到Pin以及Tokio异步

一、自引用类型

很多人知道自引用类型,那么这究竟是什么概念呢,其实就是以下的这个结构体:

struct SelfReferential {
    data: String,
    ptr: *const String, // 指向 data 的指针
}

在这里我们定义了一个SelfReferential的结构体,里面有个dataptr,而且要求ptr是指向的data

请看下面的main方法:

fn main() {
   let mut instance = SelfReferential {
        data: String::from("Hello, world!"),
        ptr: std::ptr::null(), // 初始化为 null
    };
    instance.ptr = &instance.data; // 将 ptr 指向 data

    println!("--- before move ---");
    println!("instance address: {:p}", &instance.data); 
    println!("instance pointer: {:p}", instance.ptr);

    let moved_instance = instance; 

    println!("--- after move ---");
    println!("moved instance address: {:p}", &moved_instance.data);
    println!("moved instance pointer: {:p}", moved_instance.ptr);
}

我们把instance的所有权,转移到了moved_instance,此时我们打印一下输出的地址的值:

--- before move ---
instance address: 0x16f462040
instance pointer: 0x16f462040
--- after move ---
moved instance address: 0x16f4620c0
moved instance pointer: 0x16f462040

可以看到在after move之后,整个结构体已经不再是自引用类型了。

Pin保证自引用类型特征

use std::pin::Pin;
use std::marker::PhantomPinned;

struct SelfReferential {
    data: String,
    ptr: *const String,
    _pin: PhantomPinned, // 核心改动 1:加上这个标记,禁用 Unpin
}

fn main() {
    // 核心改动 2:用 Box::pin 把数据钉在堆上,拿到一个 Pin 过的指针
    let mut instance: Pin<Box<SelfReferential>> = Box::pin(SelfReferential {
        data: String::from("Hello, world!"),
        ptr: std::ptr::null(),
        _pin: PhantomPinned,
    });

    // 核心改动 3:使用 unsafe 拿到内部的可变引用来设置指针
    // 只有在初始化这一下需要 unsafe,因为此时你必须保证地址不再变
    let data_ptr = &instance.data as *const String;
    unsafe {
        instance.as_mut().get_unchecked_mut().ptr = data_ptr;
    }

    println!("--- before move ---");
    println!("instance address: {:p}", &instance.data);
    println!("instance pointer: {:p}", instance.ptr);

    // 此时,你无法再像之前那样 `let moved = *instance;` 
    // 因为 PhantomPinned 阻止了解引用移动
    let moved_instance: Pin<Box<SelfReferential>> = instance; 

    println!("--- after move ---");
    println!("moved instance address: {:p}", &moved_instance.data);
    println!("moved instance pointer: {:p}", moved_instance.ptr);
}

加入了 _pin: PhantomPinned 后,这个结构体变成了 !Unpin。这意味着 Rust 编译器会拒绝执行任何可能导致它在内存中移动的安全代码。

请看输出:

--- before move ---
instance address: 0x13be05d80
instance pointer: 0x13be05d80
--- after move ---
moved instance address: 0x13be05d80
moved instance pointer: 0x13be05d80

可以看到,改进后 after move 的两个地址会完全一致。

二、Future和自引用结构

其实 Rust 引入 Pin 的头号动机,根本不是为了让你手动写自引用结构体,而是为了async/await 能跑起来

写的这个 SelfReferential 结构体,本质上就是 Future 在被编译后生成的状态机(State Machine)的缩影。


1. 为什么 Future 需要 Pin?

当你写一段异步代码时,编译器会把它变成一个隐藏的结构体:

async fn my_async_fn() {
    let mut data = String::from("Hello"); // 局部变量
    let ptr = &data;                     // 跨越 await 的引用
    some_other_await().await; 
    println!("{}", ptr);
}

编译器生成的“状态机”结构体大概长这样:

struct MyAsyncFuture {
    data: String,
    ptr: *const String, // 这个指针指向了同一个结构体内部的 data!
    state: State,
}

看出来了吗?每一个含有跨越 await 引用的 async 函数,在底层都是一个自引用结构体。

如果这个 Futureawait 期间被 mem::swap 或者从一个线程移动到了另一个线程,它的内部指针(ptr)就会失效。


2. 在 Tokio/Future 中 Pin 是怎么起作用的?

当你调用 future.poll(cx) 时,你会发现 poll 方法的签名是:
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>

它强制要求 self 必须被 Pin 住。

  • 执行器的保证:像 Tokio 这样的执行器(Runtime)在调用你的 poll 之前,会确保这个 Future 已经被固定在内存里(通常是通过 Box::pin 或者在栈上 pin_mut!)。
  • 安全的移动:只要 FuturePin 住了,Tokio 就可以放心地在不同的任务之间切换,即便这个 Future 内部有再复杂的自引用指针,地址也不会变。

3. 用你的例子类比 Tokio 的操作

我们可以把你的 main 函数想象成一个简单的 Tokio 调度器:

  1. 准备阶段Box::pin(...)。Tokio 接收到一个新的任务,把它放进堆里并固定。
  2. 执行阶段instance.as_mut().get_unchecked_mut()。Tokio 调用 poll 方法。在 poll 内部,状态机可以安全地利用自引用指针来访问之前暂存的变量(比如 data)。
  3. 暂停与恢复let moved_instance = instance;。在 await 挂起期间,虽然存储 Pin 的“盒子”在执行器队列里可能被移动了位置,但盒子里的东西(你的 data)没动

4. 为什么有些 Future 不需要 Pin (Unpin)?

你代码里的 _pin: PhantomPinned 就像是给结构体上了锁。
但在 Rust 中,像 i32String 甚至不含引用的普通结构体,都是 Unpin 的。

  • 如果你的 Future 没引用自己:那么它即使被移来移去也无所谓。
  • 如果你在用 Tokio 的某些组件:比如 tokio::sync::mpsc,你会发现有些类型不需要 Pin 也能工作。

三、tokio::pin!的用法场景

我们来聊聊 tokio::pin!。如果说 Box::pin 是把 Future 关进“堆里的保险箱”,那么 tokio::pin! 就是在“栈上打地钉”

在 Tokio 中,你并不总是想用 Box::pin。因为 Box 会带来堆分配开销,对于高性能组件(比如你开发的 PalLink 网关或 AcorusDB),我们更倾向于栈固定

1. 核心作用:将变量转换为 Pin<&mut T>

tokio::pin!(x) 宏会通过同名遮蔽(Shadowing),把一个普通变量 x 变成一个类型为 Pin<&mut T> 的固定引用,且保证它在当前作用域内不可移动。

2. 场景一:在 loopselect! 中复用 Future

这是最经典、最高频的场景。如果你直接在 select! 里使用一个 Future 的引用,它在每次循环时都会尝试重新 Move。通过 pin! 固定后,才能安全地多次监听。

let fut = my_async_fn();
tokio::pin!(fut); // 将 fut 钉在栈上

loop {
    tokio::select! {
        _ = &mut fut => {
            break; // Future 完成,退出循环
        }
        _ = tokio::time::sleep(Duration::from_secs(1)) => {
            println!("Future 还没好,我等一秒再来问...");
            // 因为 fut 被 pin 住了,所以它的内部自引用指针依然有效
            // 下一次循环 poll 时不会崩
        }
    }
}

3. 场景二:手动调用 poll 方法

当你编写底层组件(比如实现一个自定义的 StreamFuture 代理)需要手动触发 poll 时,poll 的签名强制要求 Pin<&mut Self>

let mut my_fut = my_async_fn();
tokio::pin!(my_fut);

// 只有 pin 之后,才能调用 poll
let res = my_fut.as_mut().poll(cx); 

4. 场景三:避免堆分配 (Zero-cost)

  • Box::pin: 申请堆内存 → 拷贝数据进去 → 得到 Pin<Box<T>>
  • tokio::pin!: 就在当前栈位置原地打钉 → 得到 Pin<&mut T>

结论: 如果你的 Future 不需要跨越函数返回(只在当前函数内被 await 或处理),请永远优先使用 tokio::pin!


💡 总结对比表

维度 Box::pin(x) tokio::pin!(x)
内存位置 堆 (Heap) 栈 (Stack)
开销 有申请/释放内存的开销 零开销
灵活性 高(可以作为返回值返回) 低(只能在当前作用域使用)
适用场景 动态创建 Task、跨函数传递 select! 循环、局部手动 Poll
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容