如何手动实现 Future 不依赖 async?
解读
在国内 Rust 面试中,这道题常被用来区分“只会写 async fn”与“真正理解异步调度机制”的候选人。
面试官希望你从零开始说明 Future trait 的契约、状态机维护、Waker 的唤醒路径,以及为什么不需要 alloc、不需要 async/await 语法糖也能让 executor 跑起来。
回答时务必先写 trait 签名,再给出**最小可运行(no_std 也可跑)**的 struct 实现,最后解释 executor 如何驱动它到 Ready。
切忌把话题发散到 Tokio、async-std 的封装细节,除非面试官追问。
知识点
- core::future::Future trait 唯一关联类型 Output,唯一方法 poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> PollSelf::Output
- Pin<Ptr> 保证结构体自引用字段在 poll 之间不会移动;手动实现时通常用 unsafe { Pin::new_unchecked } 配合 !Unpin 标记
- Waker 是 executor 提供的“回调句柄”,手动实现必须保证至少一次 cx.waker().clone().wake(),否则 executor 会永远挂起
- 状态机 用 enum 或整数常量区分 Pending/Ready;每次 poll 根据状态推进,不依赖生成器语法
- no_std 兼容:只要实现 Future 就能被任何 executor(包括裸机自制的最小调度器)驱动,无需堆分配
- 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 {}
}
要点回顾:
- 不依赖 async/await 语法,直接 impl Future
- Pin 只用 unsafe 一行,因为 DelayTicks 没有自引用,!Unpin 是自动的
- 唤醒路径清晰:tick() -> STORED_WAKER.wake() -> executor 再次 poll
- no_std 无堆分配,适合嵌入式场景,国内芯片厂面试常问
拓展思考
- 如果 Future 需要内部自引用(如缓存 buffer 指针),就必须用 pin-project 或者手写投影,保证结构体移动时字段地址不变;面试时可被追问“为什么 Pin 能防止移动”
- 在多线程 executor 中,Waker 必须实现 Send + Sync,且唤醒要加锁或原子队列;可以进一步讨论 std 的 ArcWake 或 crossbeam 的无锁方案
- 手写 Future 组合子:实现一个 Select<FA, FB>,让两个子 Future 竞争,先 Ready 的返回,另一个取消;考察点在于如何正确存储两个不同的 Waker 并防止唤醒丢失
- 零拷贝网络场景:把 DMA 缓冲区直接映射成 Future,当网卡中断到达时通过 Waker 唤醒,从而在高频交易公司面试中展示“无 alloc 异步”能力
- 国内云厂商常问:Tokio 的 task 就是 Box<dyn Future + Send>,为什么还需要 spawn 和调度队列? 可结合手写 Future 说明“只有 Future 不够,还需要 reactor 和调度器”这一分工,体现体系化认知