基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Rust Future trait 是如何工作的?

知识点图片

Rust 的 Future trait 是 Rust 异步编程的核心机制。要理解它如何工作,最关键的一点是:Rust 的 Future 是惰性的(Lazy)且基于轮询(Poll-based)的。

这与 JavaScript 的 Promise 或 C# 的 Task 不同,后者一旦创建就开始执行(Push 模型)。在 Rust 中,除非你主动去“推动”它,否则 Future 什么都不会做。

下面我将从核心定义、工作流程、状态机和 Waker 机制四个方面深入解释。


1. 核心定义:Future Trait

Future trait 的定义非常精简,其核心在于 poll 方法:

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

pub trait Future {
    type Output; // Future 完成时返回的值类型

    // 核心方法
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

这里有两个关键的返回状态 Poll

  • Poll::Ready(val): 任务完成了,val 是结果。
  • Poll::Pending: 任务还没完成,现在需要等待。

2. 工作原理:轮询模型 (The Poll Loop)

Rust 的异步运行时(Runtime,如 Tokio 或 async-std)主要由 执行器(Executor)反应器(Reactor/Driver) 组成。

Future 的工作流程如下:

  1. 执行器调用 poll: 执行器拿到一个 Future,调用它的 poll 方法。
  2. Future 尝试推进:
    • 如果 Future 内部的计算能立即完成,它就直接返回 Poll::Ready
    • 如果它需要等待(例如等待网络数据),它会返回 Poll::Pending
  3. 关键点:Waker (唤醒器):
    • 当 Future 返回 Poll::Pending 之前,它必须做一件事:注册一个 Waker
    • 这个 Waker 包含在 poll 方法的参数 cx (Context) 中。
    • Future 会把这个 Waker 交给负责等待的底层系统(比如操作系统的 epoll/kqueue,或者一个定时器线程)。
  4. 休眠: 执行器收到 Pending 后,知道这个任务卡住了,就会把这个任务挂起(不再消耗 CPU),去处理其他任务。
  5. 唤醒:
    • 当外部事件发生(例如网卡收到了数据),底层系统会调用之前注册的 Wakerwake() 方法。
    • wake() 会通知执行器:“嘿,刚才那个卡住的任务现在可能准备好了。”
  6. 再次轮询: 执行器收到通知,再次找到那个 Future,重新调用 poll。这次 Future 应该能读到数据并返回 Poll::Ready 了。

3. 举个生活中的例子

想象你在快餐店点餐:

  1. 你 (Executor) 走到柜台问 收银员 (Future):“我的汉堡好了吗?”(调用 poll)。
  2. 收银员 还没做好,给你一个 震动取餐器 (Waker),并告诉你:“还没好,去旁边等着。”(返回 Poll::Pending)。
  3. 你拿着取餐器去玩手机(处理其他任务),不占用柜台资源。
  4. 厨房做好了汉堡,按下了按钮,你的 取餐器震动了 (Waker.wake())
  5. 你看到震动,再次 走到柜台问:“我的汉堡好了吗?”(再次调用 poll)。
  6. 收银员把汉堡给你(返回 Poll::Ready(Burger))。

4. async / await 的魔法:状态机

你可能会问:“我写代码时并没有手动写 pollWaker 啊?”

这是因为 Rust 编译器帮你做了。当你写下:

plaintext
async fn my_function() {
    let data = read_from_socket().await;
    println!("{}", data);
}

编译器会将这个 async fn 编译成一个 状态机(State Machine) 结构体,并实现 Future trait。

逻辑大致如下(伪代码):

plaintext
// 编译器生成的枚举
enum MyFunctionFuture {
    Start,
    WaitingForSocket(SocketFuture), // 保存中间状态
    Done,
}

impl Future for MyFunctionFuture {
    fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
        loop {
            match self {
                Start => {
                    // 执行 await 之前的代码
                    let socket_fut = read_from_socket();
                    *self = WaitingForSocket(socket_fut); // 状态流转
                }
                WaitingForSocket(ref mut inner_fut) => {
                    // 轮询内部的 future
                    match inner_fut.poll(cx) {
                        Poll::Ready(data) => {
                            println!("{}", data); // await 之后的代码
                            *self = Done;
                            return Poll::Ready(());
                        }
                        Poll::Pending => return Poll::Pending, // 传递 Pending
                    }
                }
                Done => panic!("Future polled after completion"),
            }
        }
    }
}

这就是 零成本抽象:没有动态分发,没有额外的堆内存分配(除非你自己 Box),整个异步逻辑被编译成了一个紧凑的状态机结构体。

5. 为什么需要 Pin

你会注意到 poll 的签名是 self: Pin<&mut Self> 而不是普通的 &mut Self

这是因为 async 生成的状态机可能包含 自引用(Self-referential)
比如:

plaintext
async fn example() {
    let x = [0; 1024];
    let y = &x; // y 指向 x,x 在同一个结构体里
    some_io().await; // 此时函数暂停,状态被保存到 Future 结构体中
}

如果这个 Future 在内存中被移动了(Move),x 的地址变了,但 y 还是指向旧地址,这就会导致悬垂指针(Dangling Pointer)。

Pin 的作用就是在类型系统中保证这个 Future 一旦开始被轮询,就不会在内存中被移动,从而保证内存安全。

总结

Rust Future 的工作机制可以概括为:

  1. 惰性:不 Poll 不执行。
  2. 状态机async/await 编译成状态机结构体。
  3. 协作式多任务:通过 Poll::Pending 让出控制权。
  4. 回调通知:通过 Waker 通知执行器再次 Poll。
  5. 无栈协程:所有状态保存在 Future 结构体中,不需要为每个任务分配独立的栈,内存占用极低。
00:00
00:00