如何手动实现 Future?

解读

在国内 Rust 岗位面试中,手写 Future 是高频进阶题,重点考察候选人对异步本质、状态机、Pin 与 Waker 的理解深度。面试官通常不会要求背标准库源码,而是希望看到:

  1. 能说出 Future 的契约(poll 返回值、Waker 唤醒机制);
  2. 能用 enum 手写一个带状态转移的最小 Future;
  3. 能解释为什么必须 Pin<&mut Self> 以及上下文如何传递;
  4. 能指出常见坑:状态机忘记保存 Waker、跨 await 点移动、未处理 panic 安全。
    回答时先给契约,再给代码,再给调用示例,节奏清晰,时间控制在 5 分钟内。

知识点

  1. Future trait 定义:
    pub trait Future {
        type Output;
        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
    }
    
  2. Poll 枚举:Pending/Ready,只有 Pending 才需要注册 Waker
  3. Pin 保证状态机内部自引用结构体在轮询过程中不被移动;
  4. Waker 是线程安全句柄,wake()/wake_by_ref() 触发 executor 再次调度
  5. 手动实现即手写 enum 状态机,每个 await 点对应一个状态变体
  6. 必须实现 Unpin 或正确使用 Pin,否则编译期拒绝;
  7. 常见 executor(tokio、async-std)对 wake 的优化:队列批处理、原子标记。

答案

下面给出一个单线程场景下可运行的极简 Future:异步 sleep 指定毫秒后返回字符串。
关键点:

  • 用 enum 保存状态;
  • 用 Instant 记录起始时间;
  • 在 Pending 分支把 cx.waker() 克隆保存,防止丢失唤醒
  • 实现 Unpin,简化 Pin 处理(生产代码可用 pin-project)。
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use std::time::{Duration, Instant};
use std::thread;

/// 手动实现的异步 sleep Future
pub struct MySleep {
    state: State,
}

enum State {
    Init { dur: Duration },
    Waiting { until: Instant, waker: Option<Waker> },
    Done,
}

impl MySleep {
    pub fn new(dur: Duration) -> Self {
        MySleep {
            state: State::Init { dur },
        }
    }
}

impl Future for MySleep {
    type Output = &'static str;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        loop {
            match &mut self.state {
                State::Init { dur } => {
                    let until = Instant::now() + *dur;
                    self.state = State::Waiting {
                        until,
                        waker: Some(cx.waker().clone()),
                    };
                    // 第一次注册后主动让出,模拟 executor 调度
                    thread::yield_now();
                    return Poll::Pending;
                }
                State::Waiting { until, waker } => {
                    if Instant::now() >= *until {
                        self.state = State::Done;
                        return Poll::Ready("my_sleep finished");
                    } else {
                        // 更新 waker,防止旧 waker 失效
                        if let Some(w) = waker {
                            if !w.will_wake(cx.waker()) {
                                *w = cx.waker().clone();
                            }
                        } else {
                            *waker = Some(cx.waker().clone());
                        }
                        return Poll::Pending;
                    }
                }
                State::Done => panic!("poll after Ready"),
            }
        }
    }
}

// 安全:无自引用字段,可 Unpin
impl Unpin for MySleep {}

使用示例(单线程裸调,仅验证手工 Future 正确性):

fn main() {
    let mut f = MySleep::new(Duration::from_millis(200));
    let waker = futures::task::noop_waker();
    let mut cx = Context::from_waker(&waker);

    loop {
        match Pin::new(&mut f).poll(&mut cx) {
            Poll::Ready(msg) => {
                println!("{}", msg);
                break;
            }
            Poll::Pending => {
                // 真实 executor 会在这里 block_on 或调度其他任务
                thread::sleep(Duration::from_millis(50));
            }
        }
    }
}

面试加分句

  1. “生产环境我会用 tokio::time::sleep,但手写一遍能看清状态机与唤醒契约。”
  2. “如果状态机内部有自引用,我会用 pin-project 宏自动生成 Pin 投影,避免 unsafe。”

拓展思考

  1. 如何把上述 MySleep 改造成支持 tokio 运行时的定时器?
    答:把 until 与 waker 注册到 tokio 的 TimerHeap,由运行时统一 epoll/timerfd 唤醒,避免用户态忙等。
  2. 如果 Future 内部需要跨 await 点分配缓冲区,如何保证内存安全?
    答:缓冲区放 Pin<Box<[u8]>>,或使用 slab 池,确保指针在 poll 间保持有效
  3. 手写 Stream 与 Future 差异?
    Stream 多一个 poll_next,状态机需维护 Item 位置与 back-pressure
  4. 在 no_std 嵌入式场景,没有堆 allocator,如何静态实例化 Future?
    答:用 const generics 与 static mut 状态数组,结合临界区保护 Waker,实现零堆异步调度