如何 Generator<Yield, Return>?

解读

国内 Rust 岗位面试里,Generator 是异步与协程底层实现的核心抽象。
面试官问“如何 Generator<Yield, Return>?”并不是让你背诵标准库 API,而是考察三点:

  1. 是否知道 Rust 目前没有稳定 Generator trait,必须依赖 nightly 的 core::ops::generator::{Generator, GeneratorState}
  2. 能否手写一个满足 Generator<Yield = Y, Return = R> 的状态机,并通过 Pin<&mut self> 安全地推进;
  3. 是否理解 yieldreturn 在编译期被翻译成 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
关键点

  1. 必须 Pin<&mut Self>,否则自引用字段移动后造成悬垂;
  2. 手工状态机与 yield 语法糖在底层生成的 MIR 完全一致,区别只是人工 vs 自动;
  3. 若面试官追问“stable 怎么办”,立刻答“用 genawaiter::sync_gen! 宏或手写 Stream 状态机,避免 nightly 风险”。

拓展思考

  1. 性能对比:手写状态机 vs yield 语法糖 vs async 块,三者在 release 模式下的汇编完全一致,证明零成本抽象。
  2. 安全边界:如果状态机内部需要自引用(如 yield 后持有内部引用),必须 Pin 且用 unsafe { self.get_unchecked_mut() };任何 mem::swap 都会立刻触发 UB。
  3. 协程调度:在国产数据库(如 TiKV)的 raftstore 中,用 generator 实现无栈协程,配合 tokio::task::unconstrained 做抢占,单线程 QPS 提升 18%;面试可举例说明“无栈协程减少上下文切换开销”。
  4. 与 C++20 协程差异:Rust generator 没有 co_await 运算符重载,编译器直接生成状态机,无需 promise_type 模板,编译速度更快,但灵活性略低。
  5. 未来稳定化:跟踪 RFC 2781 和 trait 别名 进展,预计 2025 年引入 std::iter::Iteratorstd::async_iter::AsyncIterator 的统一抽象,generator 可能作为 AsyncIterator::poll_next 的底层实现,届时可无缝替换手写状态机。