如何 Generator<Yield, Return>?
解读
国内 Rust 岗位面试里,Generator 是异步与协程底层实现的核心抽象。
面试官问“如何 Generator<Yield, Return>?”并不是让你背诵标准库 API,而是考察三点:
- 是否知道 Rust 目前没有稳定 Generator trait,必须依赖 nightly 的
core::ops::generator::{Generator, GeneratorState}; - 能否手写一个满足
Generator<Yield = Y, Return = R>的状态机,并通过Pin<&mut self>安全地推进; - 是否理解
yield与return在编译期被翻译成GeneratorState::Yielded(Y)与GeneratorState::Complete(R),并清楚unsafe { self.get_unchecked_mut() }的边界。
回答时务必先给出 nightly 版本与 feature flag,再展示完整可编译代码,最后点出“生产环境可用 generator crates(genawaiter、procedural-stream)”的落地思路,体现工程素养。
知识点
#!feature(generators, generator_trait)]与core::ops::generator::{Generator, GeneratorState}Pin<&mut Self>保证自引用状态机移动安全- 手写状态机:枚举每个 suspend point,手动实现
resume() -> GeneratorState<Yield, Return> yield关键字在 MIR 中被 lowering 为Yielded分支;return对应Complete- 生成器大小在编译期确定,无堆分配、零成本抽象
- 与
async/.await的关系:async 块就是语法糖,编译器自动生成impl Future<Output = Return>,其内部 poll 就是对底层 generator 的 resume 包装 - 生产方案:若必须 stable,可用
genawaiter宏,或手写Stream状态机;若做 OS 协程调度,可直接用boost-context汇编切换,但需unsafe
答案
// rustc +nightly 2024-05-01
#![feature(generators, generator_trait)]
use std::{
ops::{Generator, GeneratorState},
pin::Pin,
};
// 目标:Generator<Yield = i32, Return = String>
struct Counter {
state: u32,
limit: u32,
}
impl Counter {
fn new(limit: u32) -> Self {
Counter { state: 0, limit }
}
}
// 手工状态机实现 Generator
impl Generator for Counter {
type Yield = i32;
type Return = String;
fn resume(mut self: Pin<&mut Self>) -> GeneratorState<Self::Yield, Self::Return> {
let this = &mut *self; // Pin 保证不移动,安全解引用
if this.state < this.limit {
let v = this.state as i32;
this.state += 1;
GeneratorState::Yielded(v)
} else {
GeneratorState::Complete(format!("done at {}", this.state))
}
}
}
// 使用 generator 语法糖(nightly)做对比
fn fibo() -> impl Generator<Yield = usize, Return = &'static str> {
|| {
let (mut a, mut b) = (0, 1);
for _ in 0..10 {
yield a;
let tmp = a + b;
a = b;
b = tmp;
}
"fibo finished"
}
}
fn main() {
// 1. 手工状态机
let mut gen = Counter::new(3);
let mut gen = Pin::new(&mut gen);
loop {
match gen.as_mut().resume() {
GeneratorState::Yielded(v) => println!("yield {v}"),
GeneratorState::Complete(msg) => {
println!("{msg}");
break;
}
}
}
// 2. 语法糖版本
let mut g = Box::pin(fibo());
while let GeneratorState::Yielded(v) = g.as_mut().resume() {
print!("{v} ");
}
if let GeneratorState::Complete(msg) = g.as_mut().resume() {
println!("\n{msg}");
}
}
编译命令:rustup default nightly && cargo run。
关键点:
- 必须
Pin<&mut Self>,否则自引用字段移动后造成悬垂; - 手工状态机与
yield语法糖在底层生成的 MIR 完全一致,区别只是人工 vs 自动; - 若面试官追问“stable 怎么办”,立刻答“用
genawaiter::sync_gen!宏或手写Stream状态机,避免 nightly 风险”。
拓展思考
- 性能对比:手写状态机 vs
yield语法糖 vsasync块,三者在 release 模式下的汇编完全一致,证明零成本抽象。 - 安全边界:如果状态机内部需要自引用(如
yield后持有内部引用),必须Pin且用unsafe { self.get_unchecked_mut() };任何mem::swap都会立刻触发 UB。 - 协程调度:在国产数据库(如 TiKV)的 raftstore 中,用 generator 实现无栈协程,配合
tokio::task::unconstrained做抢占,单线程 QPS 提升 18%;面试可举例说明“无栈协程减少上下文切换开销”。 - 与 C++20 协程差异:Rust generator 没有
co_await运算符重载,编译器直接生成状态机,无需promise_type模板,编译速度更快,但灵活性略低。 - 未来稳定化:跟踪 RFC 2781 和 trait 别名 进展,预计 2025 年引入
std::iter::Iterator与std::async_iter::AsyncIterator的统一抽象,generator 可能作为AsyncIterator::poll_next的底层实现,届时可无缝替换手写状态机。