如何手动实现 Future?
解读
在国内 Rust 岗位面试中,手写 Future 是高频进阶题,重点考察候选人对异步本质、状态机、Pin 与 Waker 的理解深度。面试官通常不会要求背标准库源码,而是希望看到:
- 能说出 Future 的契约(poll 返回值、Waker 唤醒机制);
- 能用 enum 手写一个带状态转移的最小 Future;
- 能解释为什么必须 Pin<&mut Self> 以及上下文如何传递;
- 能指出常见坑:状态机忘记保存 Waker、跨 await 点移动、未处理 panic 安全。
回答时先给契约,再给代码,再给调用示例,节奏清晰,时间控制在 5 分钟内。
知识点
- Future trait 定义:
pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } - Poll 枚举:Pending/Ready,只有 Pending 才需要注册 Waker;
- Pin 保证状态机内部自引用结构体在轮询过程中不被移动;
- Waker 是线程安全句柄,wake()/wake_by_ref() 触发 executor 再次调度;
- 手动实现即手写 enum 状态机,每个 await 点对应一个状态变体;
- 必须实现 Unpin 或正确使用 Pin,否则编译期拒绝;
- 常见 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));
}
}
}
}
面试加分句:
- “生产环境我会用 tokio::time::sleep,但手写一遍能看清状态机与唤醒契约。”
- “如果状态机内部有自引用,我会用 pin-project 宏自动生成 Pin 投影,避免 unsafe。”
拓展思考
- 如何把上述 MySleep 改造成支持 tokio 运行时的定时器?
答:把 until 与 waker 注册到 tokio 的 TimerHeap,由运行时统一 epoll/timerfd 唤醒,避免用户态忙等。 - 如果 Future 内部需要跨 await 点分配缓冲区,如何保证内存安全?
答:缓冲区放 Pin<Box<[u8]>>,或使用 slab 池,确保指针在 poll 间保持有效。 - 手写 Stream 与 Future 差异?
Stream 多一个 poll_next,状态机需维护 Item 位置与 back-pressure。 - 在 no_std 嵌入式场景,没有堆 allocator,如何静态实例化 Future?
答:用 const generics 与 static mut 状态数组,结合临界区保护 Waker,实现零堆异步调度。