Rust: Generator 已死,Coroutine 当立,Generator 又活了
参考文献:
Generators are dead, long live coroutines, generators are back
在曾经,Rust 通过不稳定特性 generators
和 generator_trait
提供 Generator 功能,大体使用方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};
use std::pin::Pin;
fn main() {
// add_one 是一个 generator
let mut add_one = |mut x: usize| loop {
// `yield x + 1` 的值是下文中 resume 的参数,
// 而 `x + 1` 则作为 resume 的返回值。
// 如果不接收 yield 的值,
// 那么只有第一次调用 resume 的参数是有效的。
x = yield x + 1;
};
match Pin::new(&mut add_one).resume(0) {
// 初次调用 add_one,0 作为参数 x 的值传入,
// `yield x + 1` 被调用,resume 返回 1,
// 此时 add_one 在 yield 处暂停,yield 未求值。
GeneratorState::Yielded(x) => println!("yielded {x}"),
// 事实上 generator 还可以 return value,
// 并在此处通过 GeneratorState::Complete(v) 接收。
_ => (),
}
match Pin::new(&mut add_one).resume(5) {
// 第二次调用 add_one,5 作为上一次 `yield x + 1` 的值传入,
// 使 x 变为 5,再一次执行 `yield x + 1`,
// resume 返回 6。
GeneratorState::Yielded(x) => println!("yielded {x}"),
_ => (),
}
}
Generator 类似闭包语法,因此也可以捕捉外部变量,也可以使用 move
关键字:
1
2
3
4
5
6
let v = [0, 1, 2, 3, 4];
let genrator = move || {
for i in v {
yield i;
}
};
现在,这两个不稳定特性已经被移除了,原因是 Rust 团队认为该 Generator 实质已经完成了 Coroutine 的工作,因此,Rust 团队将过去所有 Generator 术语修改为 Coroutine,并引入了 coroutines
和 coroutine_trait
这两个新的不稳定特性。
从语法上来说,新的 Coroutine 和以前的 Generator 并没有什么很大的不同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#![feature(coroutines, coroutine_trait)]
use std::ops::{Coroutine, CoroutineState};
use std::pin::Pin;
fn main() {
let mut add_one = |mut x: usize| loop {
x = yield x + 1;
};
match Pin::new(&mut add_one).resume(0) {
CoroutineState::Yielded(x) => println!("yielded {x}"),
_ => (),
}
match Pin::new(&mut add_one).resume(5) {
CoroutineState::Yielded(x) => println!("yielded {x}"),
_ => (),
}
}
但是,Rust 团队对 Generator 有了新的定义。现在,Generator 只是一个生成 Iterator 的快捷方式,这意味着 Generator 是一种没有参数、没有返回值的 Coroutine。
为了更方便构造 Generator,在 Rust edition 2024 中引入了 gen_blocks
特性,一个简单的斐波那契数列为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#![feature(gen_blocks)]
fn fib() -> impl Iterator<Item = usize> {
// gen block 返回一个 impl Iterator 的匿名类型。
gen {
let mut n1 = 1;
let mut n2 = 1;
let mut n3 = 2;
yield n1;
yield n2;
loop {
yield n3;
n1 = n2;
n2 = n3;
n3 = n1 + n2;
}
}
}
fn main() {
// 通过 take 截断前 10 个值。
println!("{:?}", fib().take(10).collect::<Vec<usize>>());
}
若要运行上述代码,请使用 nightly 版本,在 cargo.toml
最上方添加 cargo-features = ["edition2024"]
,并使用 RUSTFLAGS="-Zunstable-options --edition 2024" cargo run
运行。或者,你也可以通过 godbolt 进行体验。
另外,正如前文所说,Generator 是 Coroutine 的一种,因此按照 Rust 团队的设想,gen block 理应是 Coroutine 的语法糖,上述代码应当解糖为(暂时不能编译):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn fib() -> impl Iterator<Item = usize> {
std::iter::from_fn(|| {
let mut n1 = 1;
let mut n2 = 1;
let mut n3 = 2;
yield Some(n1);
yield Some(n2);
loop {
yield Some(n3);
n1 = n2;
n2 = n3;
n3 = n1 + n2;
}
})
}
同样,由于 gen block 被定性为 Coroutine 的语法糖,因此在引用外部变量时的行为也类似闭包,需要使用 move 移入(在 godbolt 上体验):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#![feature(gen_blocks)]
// gen block 引用 `input`,因此返回值类型添加生命周期 `'_` 进行约束
fn gen_multiple(input: &[usize], multiple: usize) -> impl Iterator<Item = usize> + '_ {
// 将 `multiple` move 进 gen block,否则以 `&usize` 的形式访问 `multiple`
gen move {
for i in input {
yield i * multiple;
}
}
}
fn main() {
let input = (0..10).collect::<Vec<usize>>();
println!("{:?}", gen_multiple(&input, 2).collect::<Vec<usize>>());
}
另一方面,按照设想,gen block 也可以和 async block 组合,成为 Async Generator。由于 rustc 暂时还无法支持该特性,我们直接看官方例子简单了解一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
async gen {
while let Some(item) = inner.next().await {
yield func(item).await;
}
}
// 按照设想,应该解糖为:
std::stream::from_fn(|ctx| {
while let Some(item) = await_with!(inner.next(), ctx) {
yield Ready(Some(await_with!(func(item), ctx)));
}
Ready(None)
})