Pin 的痛苦:从手写 Future 理解 async Rust 的底层规则

本文是对 Pin and suffering 的整理与翻译。

内容结构概览

  1. async Rust 为什么让人痛苦:同步代码很直观,async 代码多了“不阻塞 executor”“Future 需要被 poll”“Future 需要 pin”等规则。
  2. 同步 sleep 与 Tokio sleep 的区别std::thread::sleep 会阻塞线程,tokio::time::sleep 返回 Future,不阻塞 executor。
  3. 为什么多线程 executor 会掩盖问题:多个 worker thread 下阻塞不明显,单线程 executor 才能看出任务被串行卡住。
  4. 手写第一个 Future:实现 Future trait,理解 Outputpoll
  5. tokio::main 做了什么:它会创建 runtime,并对 async main 返回的 Future 执行 block_on
  6. Poll::ReadyPoll::Pending:Ready 表示完成,Pending 表示暂时无法完成。
  7. Pending 不等于自动重试:Future 返回 Pending 后,只有被 Waker 唤醒,才会再次被 poll。
  8. 乱用 wake_by_ref 会 busy loop:在 poll 里立刻唤醒自己,会导致疯狂重复 poll。
  9. 用线程模拟唤醒:启动一个线程 sleep 一秒后调用 wake,让 Future 重新被 poll。
  10. 把 Tokio Sleep 嵌进自己的 Future:用 tokio::time::Sleep 作为内部状态,让它负责注册 timer。
  11. 为什么 Sleep 不能直接 pollFuture::poll 需要 Pin<&mut Self>,而不是普通 &mut Self
  12. Box::pin 的“简单模式”:把 Future 放到堆上,保证内部对象地址不变。
  13. 真正的例子:实现 SlowRead<R>:包装任意 AsyncRead,让每次读取之间人为延迟。
  14. 为什么 AsyncRead 不能简单 async fn:poll 时借用 buffer,Pending 时不能保留或写入 buffer。
  15. 先用 Pin<Box<R>>Pin<Box<Sleep>> 跑通:这是 async Rust 里的“简单模式”。
  16. 离开 Box 后问题出现SlowRead 里直接持有 Sleep 后,整个结构体不再 Unpin
  17. Pin 的核心规则:一旦某个 !Unpin 值被 pin,就不能再以普通方式移动它。
  18. 错误移动 Sleep 会触发未定义行为:Tokio timer 系统可能保存了指向 Sleep 的地址,移动后会指向错误位置。
  19. 为什么 Box::pin 能解决移动问题:移动的是指针,不是堆上的对象本身。
  20. 栈上 pin 与堆上 pinpin_utils::pin_mut! / tokio::pin! 可以安全地把局部变量 pin 在栈上。
  21. pin projection 的痛苦:从 Pin<&mut Struct> 得到 Pin<&mut field> 需要小心维护不变量。
  22. pin-project 的意义:用宏生成安全的 projection,避免自己写 unsafe。
  23. 最终结论:Pin 很复杂,但它不是无意义复杂;它是在 async 状态机和自引用/地址敏感 Future 之间维持安全边界。

Rust 的 async 很容易给人一种割裂感。

同步 Rust 已经够严谨了,但至少很多东西看起来直观:调用函数,函数执行;读取文件,线程等待;睡眠 500ms,程序暂停 500ms。你可以从调用栈想象程序怎么走,也可以用很朴素的方式理解它的控制流。

但 async Rust 一上来就有很多规则:不要阻塞 executor,Future 不会自己运行,只有被 poll 才会推进;返回 Poll::Pending 以后,必须通过 Waker 让 executor 之后再次 poll;某些 Future 不能随便移动,所以 poll 之前要 pin;Pin<&mut T>UnpinBox::pinpin_project 这些概念全都冒出来。

这篇文章就是围绕这些概念展开。它不是从定义开始硬讲 Pin,而是从一个最简单的同步 sleep 开始,一步步把 async Rust 的底层机械结构拆出来:先看什么叫阻塞 executor,再手写一个 Future,再理解 Poll::Ready / Poll::Pending / Waker,再把 Tokio 的 Sleep 嵌进自己的 Future,最后实现一个 SlowRead<R>,也就是一个人为放慢读取速度的异步 reader。

真正的主角是 Pin。文章标题叫 Pin and suffering,这个 suffering 不是随便说的。Pin 很难讲,也很难第一次就理解。但它的复杂性不是凭空来的:它是为了让某些地址敏感的异步状态机,在 Rust 的移动语义下仍然安全工作。


一、同步代码很简单,async 代码看起来像巫术

先看同步版本:

use std::{thread::sleep, time::Duration};

fn main() {
    println!("Hello!");
    sleep(Duration::from_millis(500));
    println!("Goodbye!");
}

运行效果很直观:

Hello!
等待 500ms
Goodbye!

这段代码没有什么悬念。sleep 调用会让当前线程睡眠。线程不做别的事,只是等。同步世界里,这很正常。

换成 async,好像也很简单。用 Tokio:

[dependencies]
tokio = { version = "1.4.0", features = ["full"] }

然后:

use std::{thread::sleep, time::Duration};

#[tokio::main]
async fn main() {
    println!("Hello!");
    sleep(Duration::from_millis(500));
    println!("Goodbye!");
}

输出也一样:

Hello!
等待 500ms
Goodbye!

看起来没问题。但其实它有问题:它阻塞了 executor。

为什么刚才看不出来?因为只跑了一个任务。只有一个任务时,阻塞 executor 和普通同步 sleep 的表现一样。真正的问题要在并发任务里看。

试着 spawn 两个任务:

use std::{thread::sleep, time::Duration};

#[tokio::main]
async fn main() {
    let one = tokio::spawn(greet());
    let two = tokio::spawn(greet());

    let (_, _) = tokio::join!(one, two);
}

async fn greet() {
    println!("Hello!");
    sleep(Duration::from_millis(500));
    println!("Goodbye!");
}

在默认多线程 runtime 下,可能输出:

Hello!
Hello!
等待 500ms
Goodbye!
Goodbye!

这又看起来没问题。因为 Tokio 默认可能有多个 worker thread,一个任务阻塞了一个线程,另一个任务还可以在别的线程上跑。阻塞被线程池掩盖了。

换成单线程 executor:

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let one = tokio::spawn(greet());
    let two = tokio::spawn(greet());

    let (_, _) = tokio::join!(one, two);
}

这时输出变成:

Hello!
等待 500ms
Goodbye!
Hello!
等待 500ms
Goodbye!

问题暴露了。std::thread::sleep 阻塞的是当前线程,而单线程 executor 只有一个线程。第一个任务睡眠时,executor 没法去 poll 第二个任务。它不是异步等待,而是把整个 executor 按住了。

正确写法应该用 Tokio 的 sleep:

use std::time::Duration;
use tokio::time::sleep;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let one = tokio::spawn(greet());
    let two = tokio::spawn(greet());

    let (_, _) = tokio::join!(one, two);
}

async fn greet() {
    println!("Hello!");
    sleep(Duration::from_millis(500)).await;
    println!("Goodbye!");
}

这次单线程 executor 也能并发推进两个任务:

Hello!
Hello!
等待 500ms
Goodbye!
Goodbye!

这里的关键是:std::thread::sleep 真的让线程睡眠;tokio::time::sleep 返回一个 Future,它注册一个 timer,等时间到了再唤醒任务。在等待期间,executor 可以去做别的事。

这就是 async Rust 第一个重要规则:

不要在 async 任务里阻塞 executor。

二、Future 只是一个 trait

为了理解 tokio::time::sleep 到底怎么工作,先从最小的 Future 开始。

Rust 里的 Future 只是一个 trait。任何类型都可以实现它:

use std::future::Future;

struct MyFuture {}

impl Future for MyFuture {}

编译器会提示你没有实现 Outputpoll。补全后大概是:

use std::{
    future::Future,
    pin::Pin,
    task::{Context, Poll},
};

struct MyFuture {}

impl Future for MyFuture {
    type Output = ();

    fn poll(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Self::Output> {
        todo!()
    }
}

这里有三个重要点。

第一,Future 有一个关联类型 Output,表示这个 Future 完成时产出什么值。这里用 (),表示什么都不返回。

第二,poll 返回 Poll<Self::Output>Poll 是一个 enum,有两个变体:

enum Poll<T> {
    Ready(T),
    Pending,
}

Ready(value) 表示 Future 已经完成;Pending 表示现在还没完成,以后再来。

第三,poll 的 receiver 不是普通 &mut self,而是:

self: Pin<&mut Self>

这就是后面痛苦的来源。现在先把它放一边。

如果 poll 直接返回 Ready(())

impl Future for MyFuture {
    type Output = ();

    fn poll(
        self: Pin<&mut Self>,
        _cx: &mut Context<'_>,
    ) -> Poll<Self::Output> {
        Poll::Ready(())
    }
}

然后 await 它:

#[tokio::main]
async fn main() {
    let fut = MyFuture {};

    println!("Awaiting fut...");
    fut.await;
    println!("Awaiting fut... done!");
}

输出是:

Awaiting fut...
Awaiting fut... done!

说明这个 Future 被 poll 了一次,然后立即完成。


三、tokio::main 其实帮你创建了 Runtime

我们写:

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let fut = MyFuture {};
    fut.await;
}

看起来好像没有手动创建 runtime。但其实 tokio::main 是一个宏,它大概会把代码展开成:

fn main() {
    let rt = tokio::runtime::Builder::new_current_thread()
        .build()
        .unwrap();

    let fut = async {
        let fut = MyFuture {};
        fut.await;
    };

    rt.block_on(fut);
}

也就是说,async fn main 本身会变成一个返回 Future 的普通函数。真正同步的 main 创建 runtime,然后 block_on 这个 Future。

这也解释了“谁在 poll 我的 Future”。不是你手动调用了 poll,而是 runtime 在 block_on 里不断推进任务。await 不是魔法,它最终还是依赖 executor 去 poll Future。


四、Pending 不会自动被再次 poll

如果 poll 返回 Pending

impl Future for MyFuture {
    type Output = ();

    fn poll(
        self: Pin<&mut Self>,
        _cx: &mut Context<'_>,
    ) -> Poll<Self::Output> {
        println!("MyFuture::poll()");
        Poll::Pending
    }
}

运行后会看到:

Awaiting fut...
MyFuture::poll()

然后程序一直不退出。

注意,它只被 poll 了一次。它不会自动被 runtime 反复 poll。为什么?因为如果 Future 返回 Pending 后 executor 立刻不停地重试,就会形成 busy loop。想象一个 socket 读操作,对端 5 秒后才发数据。如果 executor 在这 5 秒里疯狂 poll 它,就会白白吃掉一个 CPU 核心。

所以 async Rust 的规则是:

Future 返回 Pending 后,只有被唤醒,才会再次被 poll。

唤醒机制就在 Context 里。Context 有一个 waker() 方法,返回 &WakerWakerwakewake_by_ref

如果在 poll 里马上调用:

cx.waker().wake_by_ref();
Poll::Pending

就会看到疯狂输出:

MyFuture::poll()
MyFuture::poll()
MyFuture::poll()
MyFuture::poll()
...

这又变成 busy loop。因为每次 poll 都立即唤醒自己,executor 就会立刻再 poll 它。

正确思路是:等真的有事情发生时再 wake。比如 timer 到期、socket 可读、文件准备好了。


五、用一个线程模拟 Waker

为了直观看到 Waker 怎么用,可以写一个 Future:第一次被 poll 时,启动一个线程,让线程 sleep 一秒,然后调用 wake;第二次被 poll 时,返回 Ready。

结构体里需要一个状态:

struct MyFuture {
    started: bool,
}

第一次 poll:

impl Future for MyFuture {
    type Output = ();

    fn poll(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Self::Output> {
        println!("MyFuture::poll()");

        if self.started {
            Poll::Ready(())
        } else {
            self.started = true;

            let waker = cx.waker().clone();

            std::thread::spawn(move || {
                std::thread::sleep(Duration::from_secs(1));
                waker.wake();
            });

            Poll::Pending
        }
    }
}

运行效果是:

Awaiting fut...
MyFuture::poll()
等待 1 秒
MyFuture::poll()
Awaiting fut... done!

这就是一个最小可工作的 Future。第一次 poll 时注册“未来要唤醒我”的动作,然后返回 Pending。等一秒后,另一个线程调用 wake,executor 再次 poll 它。第二次看到 started = true,于是返回 Ready。

当然,Tokio 的 sleep 不会给每个 sleep 创建一个线程。它有自己的 timer 系统。这里用线程只是为了说明 Waker 的角色。


六、把 Tokio Sleep 嵌进自己的 Future

既然 tokio::time::sleep 会返回一个具体类型 Sleep,我们可以把它放进自己的 Future:

use tokio::time::Sleep;

struct MyFuture {
    sleep: Sleep,
}

impl MyFuture {
    fn new() -> Self {
        Self {
            sleep: tokio::time::sleep(Duration::from_secs(1)),
        }
    }
}

理想中,MyFuture::poll 只要 poll 里面的 sleep

impl Future for MyFuture {
    type Output = ();

    fn poll(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Self::Output> {
        println!("MyFuture::poll()");
        self.sleep.poll(cx)
    }
}

但这不编译。原因是 poll 不是 Sleep 的普通方法。Future::poll 的 receiver 是 Pin<&mut Self>。也就是说,你不能拿一个普通的 &mut Sleep 去 poll 它,而要拿一个 pinned 的 mutable reference。

于是可以把 Sleep 装进 Pin<Box<Sleep>>

struct MyFuture {
    sleep: Pin<Box<Sleep>>,
}

impl MyFuture {
    fn new() -> Self {
        Self {
            sleep: Box::pin(tokio::time::sleep(Duration::from_secs(1))),
        }
    }
}

impl Future for MyFuture {
    type Output = ();

    fn poll(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Self::Output> {
        println!("MyFuture::poll()");
        self.sleep.as_mut().poll(cx)
    }
}

这能工作。Box::pinSleep 放到堆上,并返回一个 Pin<Box<Sleep>>。之后我们移动 Pin<Box<Sleep>> 这个指针没关系,因为真正的 Sleep 对象在堆上的地址不变。

如果一个 Future 是 Unpin,还可以借助 futures::FutureExt::poll_unpin。但 Sleep 本身不是 Unpin,所以这里要继续理解 pin。


七、真的需要手写 Future 吗?

到这里,可能会问:现实里真的需要手写 Future 吗?不是有 async / await 吗?

很多时候确实不需要。绝大多数业务 async 代码都可以写成 async fn,然后 await 别的 Future。手写 Future::poll 是比较底层的事情。

但有些 trait 需要你实现 poll 风格接口。文章写作时,Rust 1.51 还不能在 trait 里写 async method,所以很多 async trait 都采用手写 poll 的形式。一个典型例子就是 Tokio 的 AsyncRead

AsyncRead 的核心方法是 poll_read

fn poll_read(
    self: Pin<&mut Self>,
    cx: &mut Context<'_>,
    buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>>;

它不是 async fn。它直接接收一个 buffer,让实现者决定现在能不能往里面读数据。

这里有个非常关键的点:如果返回 Poll::Pending,就不能写入 buffer。因为 Pending 表示“还没准备好”,这次调用不会产生有效读取结果。如果写了 buffer,调用方也不应该把它当作已读取数据。

所以 AsyncRead 的实现逻辑通常是:

如果还没准备好:
    注册唤醒
    返回 Poll::Pending
    不碰 buffer

如果已经准备好:
    往 buffer 写入一部分数据
    返回 Poll::Ready(Ok(()))

这和普通同步 Read 完全不同。同步 read(&mut buf) 要么阻塞到读到数据,要么返回错误/EOF。异步 poll_read 不能阻塞,所以必须把“现在没准备好”显式表达出来。


八、实现一个慢速 Reader:SlowRead<R>

现在进入文章的主例子:实现一个 SlowRead<R>,包装任意异步 reader,让它每次读之前都等一小段时间。

先定义结构体:

struct SlowRead<R> {
    reader: R,
}

impl<R> SlowRead<R> {
    fn new(reader: R) -> Self {
        Self { reader }
    }
}

然后实现 AsyncRead

use tokio::io::{AsyncRead, ReadBuf};

impl<R> AsyncRead for SlowRead<R>
where
    R: AsyncRead,
{
    fn poll_read(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut ReadBuf<'_>,
    ) -> Poll<std::io::Result<()>> {
        self.reader.poll_read(cx, buf)
    }
}

这不编译。因为 poll_read 也要求 receiver 是 Pin<&mut R>,而 self.reader 只是字段访问得到的普通值。

先用“简单模式”:把 reader 存成 Pin<Box<R>>

struct SlowRead<R> {
    reader: Pin<Box<R>>,
}

impl<R> SlowRead<R> {
    fn new(reader: R) -> Self {
        Self {
            reader: Box::pin(reader),
        }
    }
}

impl<R> AsyncRead for SlowRead<R>
where
    R: AsyncRead,
{
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut ReadBuf<'_>,
    ) -> Poll<std::io::Result<()>> {
        self.reader.as_mut().poll_read(cx, buf)
    }
}

这能编译,也能工作。先不加延迟,直接读 /dev/urandom,比如读 128KB,耗时几毫秒。

接着加入 sleep:

struct SlowRead<R> {
    reader: Pin<Box<R>>,
    sleep: Pin<Box<Sleep>>,
}

每次 poll_read 时,先 poll sleep。如果 sleep Ready,就 reset 下一个 deadline,然后 poll reader;如果 sleep Pending,就返回 Pending。

impl<R> AsyncRead for SlowRead<R>
where
    R: AsyncRead,
{
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut ReadBuf<'_>,
    ) -> Poll<std::io::Result<()>> {
        match self.sleep.as_mut().poll(cx) {
            Poll::Ready(_) => {
                self.sleep
                    .as_mut()
                    .reset(Instant::now() + Duration::from_millis(25));

                self.reader.as_mut().poll_read(cx, buf)
            }
            Poll::Pending => Poll::Pending,
        }
    }
}

现在读 128KB 会变慢,比如从几毫秒变成几百毫秒。说明每次读取之间确实插入了延迟。

这就是“easy mode async”。把内部 future 和 reader 都 Box-pin 起来,少管一点 pin projection 的细节。就像有时候用 Arc<Mutex<T>> 可以先绕过复杂生命周期一样,Pin<Box<T>> 也能让 async 底层代码先跑起来。


九、为什么 Pin 存在:不是复杂给复杂看

到这里,代码跑了,但问题还没解释清楚:为什么非要 Pin?为什么 Sleep 不是 Unpin?为什么不能直接 &mut self.sleep

为了理解这个问题,先离开 Box。

SlowRead 改成直接持有 reader 和 sleep:

struct SlowRead<R> {
    reader: R,
    sleep: Sleep,
}

我们希望在 poll_read 里分别 poll sleepreader。但 self 的类型是 Pin<&mut Self>。如果 SelfUnpin,Rust 可以安全地把 Pin<&mut Self> 当成普通 &mut Self 用,因为 Unpin 的意思就是:这个类型即使被 pin,也仍然可以移动。

一开始只有 reader: R 时,只要 R: Unpin,整个 SlowRead<R> 也是 Unpin。所以访问 self.reader 好像能工作。

但一旦加入 Sleep,情况变了。Sleep 不是 Unpin。于是 SlowRead<R> 也不是 Unpin。这时不能再安全地从 Pin<&mut SlowRead<R>> 得到 &mut SlowRead<R>,因为那样就可能移动内部的 Sleep

编译器于是拒绝:

cannot borrow data in a dereference of Pin<&mut SlowRead<R>> as mutable

这不是编译器故意刁难,而是它在保护 pin 的不变量。


十、Pin 的核心规则

可以把 Pin 的核心规则粗略理解为:

一旦一个 !Unpin 的值被 pin,
你就不能再以普通方式移动它。

更具体一点:

如果你构造了 Pin<&mut T>,
并且 T 不是 Unpin,
那么你必须保证这个 T 后续不会被移动。

为什么?因为有些类型内部可能把自己的地址注册到了某个外部系统里。Tokio 的 Sleep 就是这种类型的好例子。

Sleep 第一次被 poll 时,可能会把自己注册到 Tokio 的 timer 系统里。timer 系统需要在时间到期时唤醒对应任务。它可能间接保存了与这个 Sleep 相关的地址或状态。如果这个 Sleep 后来被移动到别的内存位置,timer 系统持有的指向旧位置的信息就失效了。

于是,pin 的目的就是让这种地址敏感的 Future 可以安全存在。

这就是为什么 Box::pin 有用。Box::pin(sleep)Sleep 放到堆上。之后你移动的是 Box 这个指针,而不是堆上的 Sleep 本体。Sleep 的地址不变,所以 timer 系统引用它是安全的。


十一、错误移动 Sleep 会发生什么

文章构造了一个故意违反 pin 规则的例子。

先创建两个 sleep:

let mut sleep1 = sleep(Duration::from_secs(1));
let mut sleep2 = sleep(Duration::from_secs(1));

然后把 sleep1Pin::new_unchecked pin 一次,并 poll 它,让它注册到 Tokio timer 系统里。接着又把 sleep1sleep2 swap:

swap(&mut sleep1, &mut sleep2);

这就违反了规则:sleep1 被 pin 并 poll 过,之后又被普通方式移动了。

结果在 debug 构建里,Tokio 内部触发断言失败;release 构建里,程序可能直接挂住,不输出,也不退出。

这就是未定义行为的可怕之处。它不一定崩溃,不一定给你清晰错误。它可能看似正常,也可能挂住,也可能触发内部不变量破坏。Rust 安全代码不允许你这么做;只有用了 unsafe,你才可能绕过编译器保护,把自己送进“请自求多福”的地带。

这里可以总结:

Pin 的存在,是为了让地址敏感的值在被 poll 后不会被移动。

对于 async Rust 来说,很多 Future 本质上是状态机。它们可能在第一次 poll 后注册 waker、timer、I/O interest 等信息。它们的内部状态不能被随意搬来搬去。Pin 就是类型层面对这件事的表达。


十二、pin projection:真正痛苦的地方

问题是,在实际结构体里,我们经常不是只 pin 整个对象,而是要 poll 它的某个字段。

比如 SlowRead<R>

struct SlowRead<R> {
    reader: R,
    sleep: Sleep,
}

poll_read 收到的是:

self: Pin<&mut Self>

但我们要分别得到:

Pin<&mut R>
Pin<&mut Sleep>

这个过程叫 pin projection,也就是从 pinned struct 投影到 pinned field。

如果自己手写,需要 unsafe:

let (mut sleep, reader) = unsafe {
    let this = self.get_unchecked_mut();

    (
        Pin::new_unchecked(&mut this.sleep),
        Pin::new_unchecked(&mut this.reader),
    )
};

然后:

match sleep.as_mut().poll(cx) {
    Poll::Ready(_) => {
        sleep.reset(Instant::now() + Duration::from_millis(25));
        reader.poll_read(cx, buf)
    }
    Poll::Pending => Poll::Pending,
}

这能工作,但这里的 unsafe 不是装饰。你必须保证:被投影成 pinned 的字段后续不会再被普通方式移动。你还要保证自己没有同时制造别名问题,没有把 pinned 字段和 unpinned 字段混着用,没有在某个方法里把字段取出来导致移动。

这就是 “just be careful” 领域。而 Rust 的安全哲学正是:尽量不要靠人小心。


十三、栈上 pin 与堆上 pin

如果 SlowRead 自身不是 Unpin,使用它时也要先 pin。

一种方式是 unsafe:

let mut f = SlowRead::new(File::open("/dev/urandom").await?);
let mut f = unsafe { Pin::new_unchecked(&mut f) };
f.read_exact(&mut buf).await?;

但这又把 unsafe 暴露给调用方了,不好。

pin_utils crate 提供了安全宏:

pin_utils::pin_mut!(f);

Tokio 也有类似的 tokio::pin!。它会把局部变量 pin 在栈上,并且通过 shadowing 阻止你之后继续用原来的 unpinned 变量。

使用方式:

let f = SlowRead::new(File::open("/dev/urandom").await?);
pin_utils::pin_mut!(f);

f.read_exact(&mut buf).await?;

另一种方式是把整个 SlowRead 放到堆上:

let mut f = Box::pin(SlowRead::new(File::open("/dev/urandom").await?));
f.read_exact(&mut buf).await?;

这两种方式都安全。区别是:栈上 pin 不需要堆分配,但值不能轻易传来传去;Box::pin 有堆分配,但更容易把对象作为一个可移动的句柄传递,因为移动的是 box 指针,不是内部对象。

这也解释了很多 async Rust 代码里为什么常见 Box::pin。它不是性能最优,但它是理解和使用成本较低的方案。


十四、into_inner 为什么危险

假设我们想给 SlowRead<R> 加一个方法,把里面的 reader 取出来:

impl<R> SlowRead<R>
where
    R: Unpin,
{
    fn into_inner(self) -> R {
        self.reader
    }
}

这看起来合理。读完之后,把内部 reader 还给调用方。

但如果 SlowRead 曾经被 pin 并 poll 过,这个方法就很危险。因为调用 into_inner(self) 会移动整个 SlowRead,并移动出 reader。如果里面还有被 pin 过的 Sleep,就必须非常小心它是否已经被安全 drop,以及是否仍然被外部系统引用。

pin_utils::pin_mut! 在这方面会帮忙:一旦它把变量 pin 住并 shadow,之后你不能再用原变量调用 into_inner。编译器会报 use of moved value。

如果非要支持取回内部 reader,可以设计更谨慎的 API,比如把 reader 存成 Option<R>,提供一个接收 Pin<&mut Self>take_inner

struct SlowRead<R> {
    reader: Option<R>,
    sleep: Sleep,
}

impl<R> SlowRead<R>
where
    R: Unpin,
{
    fn take_inner(self: Pin<&mut Self>) -> Option<R> {
        unsafe {
            self.get_unchecked_mut().reader.take()
        }
    }
}

然后 poll_read 里如果 reader 已被取走,就模拟 EOF:

match reader {
    Some(reader) => {
        let reader = Pin::new(reader);
        reader.poll_read(cx, buf)
    }
    None => Poll::Ready(Ok(())),
}

这说明 pin 相关 API 设计很容易变复杂。你不仅要想“这个方法能不能写”,还要想“调用这个方法是否会破坏 pin 不变量”。

如果确实需要 ergonomics,另一个选择是回到 Pin<Box<Sleep>>,让整个 SlowRead 自己重新变成 Unpin。代价是多一次堆分配,但 API 会简单很多。

这也是工程里的常见权衡:不是所有地方都要追求零分配。少量分配换来更清晰、更安全的 API,有时很值得。


十五、pin-project:别自己手写 unsafe

文章最后引入 pin-project crate。

我们之前手写 pin projection:

let (mut sleep, reader) = unsafe {
    let this = self.get_unchecked_mut();

    (
        Pin::new_unchecked(&mut this.sleep),
        Pin::new_unchecked(&mut this.reader),
    )
};

这段 unsafe 的正确性要靠人维护。字段多了、状态复杂了、方法多了,很容易出错。

pin-project 的思路是:在结构体上加属性宏,并标注哪些字段需要被 pin:

use pin_project::pin_project;

#[pin_project]
struct SlowRead<R> {
    #[pin]
    reader: R,

    #[pin]
    sleep: Sleep,
}

然后在 poll_read 里调用自动生成的 project()

impl<R> AsyncRead for SlowRead<R>
where
    R: AsyncRead + Unpin,
{
    fn poll_read(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut ReadBuf<'_>,
    ) -> Poll<std::io::Result<()>> {
        let mut this = self.project();

        match this.sleep.as_mut().poll(cx) {
            Poll::Ready(_) => {
                this.sleep
                    .reset(Instant::now() + Duration::from_millis(25));

                this.reader.poll_read(cx, buf)
            }
            Poll::Pending => Poll::Pending,
        }
    }
}

project() 会把:

Pin<&mut SlowRead<R>>

安全地转换成类似:

struct SlowReadProjected<'a, R> {
    reader: Pin<&'a mut R>,
    sleep: Pin<&'a mut Sleep>,
}

这样我们不需要自己写 unsafe,也不容易不小心把 pinned 字段当普通字段使用。

最终:

grep "unsafe" -Rn src

没有 unsafe,程序仍然能跑。

这就是 pin-project 的意义:把危险的 pin projection 封装进经过审查的宏生成代码里。调用方只需要在类型设计时明确哪些字段是 pinned,之后用生成的 projection 方法操作。


十六、这篇文章真正讲清了什么

这篇文章不是单纯介绍 Pin 的定义。它做了一条更有用的路线:从“我为什么不能在 async 里用普通 sleep”开始,一步步走到 Pin 的必要性。

这条路线可以总结成:

async 任务不能阻塞 executor
    -> 等待必须用 Future 表达
        -> Future 只有被 poll 才会推进
            -> Pending 后必须用 Waker 唤醒
                -> 某些 Future 会把自己注册到 executor / timer / IO 系统
                    -> 注册后如果 Future 被移动,外部系统可能指向错误地址
                        -> 所以需要 Pin 保证地址稳定
                            -> 结构体里有 pinned 字段时,需要 pin projection
                                -> 手写 projection 需要 unsafe
                                    -> pin-project 帮我们安全生成 projection

这一条链非常重要。很多人第一次学 Pin 时,会直接看到一堆抽象定义:pinned pointer、Unpin auto trait、projection、PhantomPinned、unsafe invariants。看完更晕。

但如果从 Future 的运行方式开始,就会更容易理解:Pin 不是为了让 async Rust 看起来吓人,而是因为 async 状态机可能在 Pending 之后被保存起来,等待外部事件再次唤醒。某些状态机的内部地址不能变。Rust 默认值可以移动,所以需要一种机制表达“这个值从现在开始不能被移动”。

这就是 Pin。


十七、Unpin 是什么

Unpin 可以理解为一个标记:这个类型即使被 pin,也仍然可以安全移动。

大多数普通类型都是 Unpin。比如 u32StringVec<T>、大部分普通 struct,只要它们内部没有地址敏感状态,都可以移动。移动它们不会破坏外部引用,因为没有外部系统依赖它们的内存地址。

tokio::time::Sleep 不是 Unpin。因为它可能和 Tokio timer 系统建立了地址相关关系。被 poll 之后,它不能随便换位置。

Pin<&mut T>T: Unpin 的类型没那么可怕。因为如果 T: Unpin,你可以安全地从 Pin<&mut T> 得到 &mut T。这也是为什么一开始 SlowRead<R> 只有 reader: RR: Unpin 时,访问 self.reader 看起来很自然。

但一旦 Self 包含一个 !Unpin 字段,比如 Sleep,整个 Self 也变成 !Unpin。这时从 Pin<&mut Self> 得到普通 &mut Self 就不再安全。你必须通过 pin projection 分别处理字段。

所以:

Pin 关心的是“这个值能不能移动”
Unpin 表示“即使被 pin,也仍然能移动”
!Unpin 表示“被 pin 后不能随便移动”

注意,!Unpin 不是说值永远不能移动。它在被 pin 之前当然可以移动。关键是:一旦你以 pinned 方式开始使用它,就必须遵守不再移动的承诺。


十八、为什么 async Rust 有时让人觉得难

async Rust 的难,不完全来自语法。async fn.await 本身其实挺好用。真正难的是当你离开表层语法,开始实现底层 trait、写自定义 Future、写自定义 AsyncRead/AsyncWrite、处理状态机和内部 Future 时,底层规则会全部浮上来。

在同步 Rust 里,一个函数调用要么返回,要么 panic,要么阻塞。借用关系通常跟调用栈很好对应。一个 &mut buf 借出去,函数返回就结束。

在 async Rust 里,Future 可能执行一半返回 Pending。它把自己的状态保存起来,之后再继续。这个“执行一半暂停”的能力,带来了很多新问题:

借用可能跨越 await
状态机可能保存内部引用
Future 可能被移动
executor 可能在不同时间 poll 它
Pending 后必须靠 Waker 唤醒
某些 Future 注册到外部系统后不能移动

这些问题在普通业务 async fn 中通常被编译器和 runtime 处理了。但一旦你手写 poll,就等于走到机器房里,开始自己维护齿轮。

所以文章里的痛苦感是有来源的。不是作者故意夸张,而是 async Rust 的底层确实有一些必要复杂性。


十九、对实际开发的建议

第一,不要一开始就手写 Future。

多数应用代码用 async fn、.await、Tokio 提供的工具就够了。手写 Future::pollAsyncRead::poll_read 是库作者或底层封装更常遇到的事情。

第二,如果只是想让东西跑起来,Box::pin 是完全合理的。

它有堆分配,但能让你先避开很多 pin projection 细节。性能敏感时再优化,不要一上来就把自己扔进 unsafe。

第三,如果需要栈上 pin,用 pin_utils::pin_mut!tokio::pin!

不要自己写 unsafe { Pin::new_unchecked(&mut value) },除非你非常清楚不变量。

第四,如果结构体有 pinned 字段,用 pin-project 或类似 crate。

手写 pin projection 很容易出错。宏不是偷懒,而是把 unsafe 封装到更可靠的地方。

第五,理解 Unpin 的直觉。

大多数类型都是 Unpin,所以你平时不用管。真正需要管的是像 Sleep 这种 !Unpin 类型,以及包含它们的结构体。

第六,别把 Poll::Pending 当成“过会儿自动再试”。

返回 Pending 时,必须确保某个事件之后会 wake 当前 task。否则 Future 可能永远只被 poll 一次,然后挂在那里。

第七,不要在 poll 里立刻 wake 自己,除非你真的知道自己在做什么。

这会形成 busy loop,疯狂占 CPU。


二十、文章的时代背景

这篇文章写于 Rust 1.51 时代。那时 async Rust 已经可用,但很多东西还处在“青春期”:能用,但很多地方不够顺手。比如 trait 里不能直接写 async method,所以大量异步 trait 采用 poll_* 风格或 associated Future 风格。

后来 Rust 1.75 稳定了 async fn in trait,这让一部分场景更舒服。但这不意味着 Pin 消失了,也不意味着手写 poll 不再存在。底层库、runtime、I/O trait、状态机、组合器仍然需要这些概念。

也就是说,文章里的某些历史限制已经改变,但核心解释仍然有价值。Future 仍然靠 poll 推进,Pending 仍然需要 Waker,地址敏感的 Future 仍然需要 Pin,pin projection 仍然是底层异步代码绕不开的主题。


二十一、总结

这篇文章从一个最简单的例子开始:同步 Rust 里打印 Hello,sleep 500ms,再打印 Goodbye,很直观。换到 async Rust,如果直接在 async 函数中调用 std::thread::sleep,虽然单任务下看起来没问题,但它会阻塞 executor。默认多线程 Tokio runtime 可能掩盖这个问题,而单线程 current_thread runtime 会清楚暴露:两个任务会串行执行。正确做法是使用 tokio::time::sleep().await,让 sleep 变成一个 Future,而不是阻塞线程。

接着,文章手写了一个最小 Future。Future 只是 trait,有 Outputpollpoll 返回 Poll::Ready(value) 表示完成,返回 Poll::Pending 表示现在还不能完成。tokio::main 会创建 runtime,并对 async main 返回的 Future 执行 block_on。Future 不是自己运行,而是由 executor poll。返回 Pending 后,Future 不会自动被反复 poll;只有通过 Context 里的 Waker 唤醒,executor 才会再次 poll 它。如果在 poll 里立刻 wake_by_ref,就会造成 busy loop;正确做法是在真正有事件发生时唤醒,比如 timer 到期或 I/O 就绪。

为了理解 timer,文章先用线程模拟:第一次 poll 时启动线程,线程 sleep 一秒后调用 waker,第二次 poll 时返回 Ready。然后把 Tokio 的 Sleep 嵌进自己的 Future。此时问题出现:Sleep 不能直接用普通 &mut poll,因为 Future::poll 要求 Pin<&mut Self>。用 Box::pinSleep 放到堆上后,可以通过 as_mut().poll(cx) 工作。这里引出了 Pin 的核心:某些 Future 一旦被 poll,可能会把自己注册到 executor 或 timer 系统中,外部系统可能持有与它地址相关的信息。如果之后移动这个 Future,就会破坏不变量。

文章随后实现了一个真实一点的例子:SlowRead<R>。它包装任意 AsyncRead,在每次读取之间加入延迟。最简单的版本把内部 reader 和 sleep 都存成 Pin<Box<_>>,这相当于 async Rust 的“简单模式”:有堆分配,但能安全跑通。再进一步,为了去掉 Box,直接在结构体里持有 reader: Rsleep: Sleep。这时 SlowRead 因为包含 Sleep 而变成 !Unpin,不能再从 Pin<&mut SlowRead<R>> 安全得到普通 &mut SlowRead<R>。想 poll 字段,就需要 pin projection:从 Pin<&mut Struct> 得到 Pin<&mut field>

手写 pin projection 需要 unsafe,比如 get_unchecked_mutPin::new_unchecked。一旦写 unsafe,就必须自己维护不变量:被 pin 的字段不能再以普通方式移动。文章用一个故意错误移动 Sleep 的例子展示了未定义行为:debug 构建中 Tokio 内部断言失败,release 构建中程序可能直接挂住。原因是 Sleep 第一次 poll 后注册进 timer 系统,移动后 timer 系统可能指向错误内存位置。

最后,文章给出安全方案。栈上 pin 可以用 pin_utils::pin_mut!tokio::pin!,避免自己写 Pin::new_unchecked。如果结构体有 pinned 字段,应该使用 pin-project。通过 #[pin_project] 标注结构体,再给需要 pinned 的字段加 #[pin],就能调用生成的 project() 方法,安全地把 Pin<&mut SlowRead<R>> 投影成包含 Pin<&mut R>Pin<&mut Sleep> 的结构。这样就不需要在业务代码中写 unsafe,也不会不小心把 pinned 字段当普通字段使用。

整篇文章真正讲清楚的是:Pin 不是 async Rust 为了难而难。它解决的是 Rust 默认“值可以移动”和 async 状态机“某些值被 poll 后地址不能变”之间的冲突。普通业务代码通常可以远离这些细节,但一旦你要实现底层 Future、AsyncRead、AsyncWrite 或自己的状态机,就必须理解这套规则。Pin 很痛,但它的痛来自真实的复杂性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值