如何手动实现 Future 不依赖 async?

解读

在国内 Rust 面试中,这道题常被用来区分“只会写 async fn”与“真正理解异步调度机制”的候选人。
面试官希望你从零开始说明 Future trait 的契约、状态机维护、Waker 的唤醒路径,以及为什么不需要 alloc、不需要 async/await 语法糖也能让 executor 跑起来。
回答时务必先写 trait 签名,再给出**最小可运行(no_std 也可跑)**的 struct 实现,最后解释 executor 如何驱动它到 Ready。
切忌把话题发散到 Tokio、async-std 的封装细节,除非面试官追问。

知识点

  1. core::future::Future trait 唯一关联类型 Output,唯一方法 poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> PollSelf::Output
  2. Pin<Ptr> 保证结构体自引用字段在 poll 之间不会移动;手动实现时通常用 unsafe { Pin::new_unchecked } 配合 !Unpin 标记
  3. Waker 是 executor 提供的“回调句柄”,手动实现必须保证至少一次 cx.waker().clone().wake(),否则 executor 会永远挂起
  4. 状态机 用 enum 或整数常量区分 Pending/Ready;每次 poll 根据状态推进,不依赖生成器语法
  5. no_std 兼容:只要实现 Future 就能被任何 executor(包括裸机自制的最小调度器)驱动,无需堆分配
  6. Drop 安全性:如果 Future 在 Pending 时被 drop,必须清理已注册的资源(定时器、IO 事件等),防止唤醒丢失

答案

下面给出单线程、no_std、无 alloc 的手写 Future 示例:实现一个“睡眠指定 tick 次数”的延时器,可被裸机主循环执行。

#![no_std]
#![no_main]

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

/// 全局共享变量,由主循环在每次 tick 减一
static mut REMAINING_TICKS: u32 = 0;
/// 保存最后一次注册的 waker,方便主循环唤醒
static mut STORED_WAKER: Option<Waker> = None;

/// 手写 Future:DelayTicks
pub struct DelayTicks {
    target: u32,
}

impl DelayTicks {
    pub fn new(ticks: u32) -> Self {
        DelayTicks { target: ticks }
    }
}

impl Future for DelayTicks {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        unsafe {
            // 第一次 poll:初始化计数器与 waker
            if REMAINING_TICKS == 0 {
                REMAINING_TICKS = self.target;
                STORED_WAKER = Some(cx.waker().clone());
                return Poll::Pending;
            }
            // 后续 poll:检查计数器
            if REMAINING_TICKS == 0 {
                Poll::Ready(())
            } else {
                // 更新 waker,防止旧 waker 失效
                STORED_WAKER = Some(cx.waker().clone());
                Poll::Pending
            }
        }
    }
}

/// 主循环调用,每秒一次
pub fn tick() {
    unsafe {
        if REMAINING_TICKS > 0 {
            REMAINING_TICKS -= 1;
            if REMAINING_TICKS == 0 {
                if let Some(w) = STORED_WAKER.take() {
                    w.wake(); // 手动唤醒,executor 会再次 poll
                }
            }
        }
    }
}

/// 极简 executor:不断轮询直到 Ready
pub fn block_on<F: Future>(mut fut: F) -> F::Output {
    let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
    let waker = unsafe { Waker::from_raw(core::task::RawWaker::new(core::ptr::null(), &VTABLE)) };
    let mut cx = Context::from_waker(&waker);
    loop {
        match fut.as_mut().poll(&mut cx) {
            Poll::Ready(val) => return val,
            Poll::Pending => {} // 裸机主循环会在 tick() 里唤醒
        }
    }
}

static VTABLE: core::task::RawWakerVTable =
    core::task::RawWakerVTable::new(|_| core::task::RawWaker::new(core::ptr::null(), &VTABLE), |_| {}, |_| {}, |_| {});

/// 使用示例
#[no_mangle]
pub extern "C" fn main() -> ! {
    let delay = DelayTicks::new(5); // 延时 5 个 tick
    block_on(delay);                // 阻塞到计数归零
    // 这里已经 Ready,可以干别的事
    loop {}
}

要点回顾:

  1. 不依赖 async/await 语法,直接 impl Future
  2. Pin 只用 unsafe 一行,因为 DelayTicks 没有自引用,!Unpin 是自动的
  3. 唤醒路径清晰:tick() -> STORED_WAKER.wake() -> executor 再次 poll
  4. no_std 无堆分配,适合嵌入式场景,国内芯片厂面试常问

拓展思考

  1. 如果 Future 需要内部自引用(如缓存 buffer 指针),就必须用 pin-project 或者手写投影,保证结构体移动时字段地址不变;面试时可被追问“为什么 Pin 能防止移动”
  2. 多线程 executor 中,Waker 必须实现 Send + Sync,且唤醒要加锁或原子队列;可以进一步讨论 std 的 ArcWake 或 crossbeam 的无锁方案
  3. 手写 Future 组合子:实现一个 Select<FA, FB>,让两个子 Future 竞争,先 Ready 的返回,另一个取消;考察点在于如何正确存储两个不同的 Waker 并防止唤醒丢失
  4. 零拷贝网络场景:把 DMA 缓冲区直接映射成 Future,当网卡中断到达时通过 Waker 唤醒,从而在高频交易公司面试中展示“无 alloc 异步”能力
  5. 国内云厂商常问:Tokio 的 task 就是 Box<dyn Future + Send>,为什么还需要 spawn 和调度队列? 可结合手写 Future 说明“只有 Future 不够,还需要 reactor 和调度器”这一分工,体现体系化认知