await 是 如何实现 暂停 | 挂起 | 等待 功能
一开始的 Generator
其实底层实现是通过生成器来实现的
下面是维基百科对生成器定义的说明:https://rustwiki.org/zh-CN/std/ops/trait.Generator.html
This is a nightly-only experimental API. (<font style="color:rgb(0, 0, 0);background-color:rgb(255, 245, 214);">generator_trait</font>
#43122)
pub trait Generator<R = ()> {
type Yield;
type Return;
// Required method
fn resume(
self: Pin<&mut Self>,
arg: R
) -> GeneratorState<Self::Yield, Self::Return>;
}
- 由内置生成器类型实现的 trait
- 生成器,通常也称为协程,目前是 Rust 中的一个实验性语言特性。 RFC 2033 中添加的生成器目前主要用于为 async/await 语法提供构建块,但可能会扩展为迭代器和其他原语提供符合人体工程学的定义
- 生成器的语法和语义不稳定,将需要进一步的 RFC 来稳定。但是,此时的语法类似于闭包
#![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};
use std::pin::Pin;
fn main() {
let mut generator = || {
yield 1;
"foo"
};
match Pin::new(&mut generator).resume(()) {
GeneratorState::Yielded(1) => {}
_ => panic!("unexpected return from resume"),
}
match Pin::new(&mut generator).resume(()) {
GeneratorState::Complete("foo") => {}
_ => panic!("unexpected return from resume"),
}
}
标准库将 Gnerator 改名
后来 Rust 官方将上面的生成器重新改名为 Coroutine
https://doc.rust-lang.org/unstable-book/language-features/coroutines.html
此功能的跟踪 issue 是:#43122
- Rust 中的协程(**coroutines**)特性允许你定义 协程 或 协程字面量。协程是一种“可恢复函数”,它在语法上类似于闭包,但在编译器中被编译为完全不同的语义。协程的主要特性是它可以在执行期间挂起,然后在稍后的时期恢复执行。协程使用 yield 关键字来“返回”,然后调用者可以在 yield 关键字之后恢复协程以恢复执行
- 目前,协程是编译器中的一个额外的不稳定特性。在 RFC 2033 中添加的它们现在主要是作为信息/约束收集阶段。这样做的目的是,在实际稳定之前,可以在 nightly 编译器上进行实验。还需要进一步的 RFC 来稳定协程,可能会对整体设计进行至少一些小的调整
#![feature(coroutines, coroutine_trait, stmt_expr_attributes)]
use std::ops::{Coroutine, CoroutineState};
use std::pin::Pin;
fn main() {
let mut coroutine = #[coroutine] || {
yield 1;
return "foo"
};
match Pin::new(&mut coroutine).resume(()) {
CoroutineState::Yielded(1) => {}
_ => panic!("unexpected value from resume"),
}
match Pin::new(&mut coroutine).resume(()) {
CoroutineState::Complete("foo") => {}
_ => panic!("unexpected value from resume"),
}
}
协程是类似闭包的字面量,用 #[coroutine] 注释,可以包含 yield 语句。yield 语句接受一个可选的表达式作为参数,将其输出到协程之外。所有的协程字面量都在 std::ops 模块中实现了协程特征。协程特征有一个主要方法 resume,它在前一个暂停点恢复协程的执行。协程控制流的一个例子是以下示例按顺序打印所有数字
#![feature(coroutines, coroutine_trait, stmt_expr_attributes)]
use std::ops::Coroutine;
use std::pin::Pin;
fn main() {
let mut coroutine = #[coroutine] || {
println!("2");
yield;
println!("4");
};
println!("1");
Pin::new(&mut coroutine).resume(());
println!("3");
Pin::new(&mut coroutine).resume(());
println!("5");
}
1
2
3
4
5
目前,协程的主要用例是 async/await 和 gen 语法的实现原语,但协程在未来可能会扩展到其他原语。关于设计和使用的反馈总是很受欢迎的
The Coroutine trait
std::ops 中的协程特征目前看起来是这样的
pub trait Coroutine<R = ()> {
type Yield;
type Return;
fn resume(self: Pin<&mut Self>, resume: R) -> CoroutineState<Self::Yield, Self::Return>;
}
- Coroutine::Yield 类型是 Yield 语句可以生成的值的类型
- 该关联类型对应于 yield 表达式和协程每次执行 yield 时允许返回的值。例如,作为协程的迭代器可能有这种类型 T,正在迭代的类型
- Coroutine::Return 类型是协程的返回类型。这通常是协程定义中的最后一个表达式,或者协程中传递给返回的任何值
- 这对应于从协程返回的类型,要么用 return 语句返回,要么隐式地作为协程字面量的最后一个表达式。例如,futures 会使用Result
,因为它代表一个完成的 future
- 这对应于从协程返回的类型,要么用 return 语句返回,要么隐式地作为协程字面量的最后一个表达式。例如,futures 会使用Result
- resume 函数是执行协程本身的入口点
- 该函数将恢复协程的执行,如果还没有开始执行,则开始执行。这个调用将返回到协程的最后一个暂停点,从最新的 yield 恢复执行。协程将继续执行,直到它 yield 或 return,此时该函数将返回
- 返回值:这个函数返回的 CoroutineState 枚举表示协程返回时处于什么状态。如果生成的变体被返回,那么协程已经到达一个暂停点,并且生成了一个值。处于这种状态的协程可以在稍后恢复。如果返回 Complete,则协程已经完全完成了所提供的值。协程再次恢复是无效的
- Panics:如果在之前返回
<font style="color:rgb(0, 0, 0);">Complete</font>
之后调用该函数,则可能会出现错误。虽然语言中的协程字面量在完成后恢复时保证会出现panic,但对于协程 trait 的所有实现来说,这并不是保证
resume 的返回值 CoroutineState 如下所示
pub enum CoroutineState<Y, R> {
Yielded(Y),
Complete(R),
}
- yield 变量表示协程稍后可以恢复。这对应于协程中的屈服点
- Yielded(Y):这种状态表示协程已经被挂起,通常对应于一条 yield 语句。这个变体中提供的值对应于传递给 yield 的表达式,协程可以在每次 yield 时提供一个值
- Complete 表示协程已经完成,不能再次恢复
- Complete(R):这个状态表示协程已经用提供的值完成了执行。一旦协程返回完成,再次调用 resume 被认为是一个程序员错误
- 在协程返回 Complete 之后调用 resume 可能会导致程序出现错误
Closure-like 语义
协程的类闭包语法暗示了这样一个事实:协程也具有类闭包语义
- 当创建协程时,协程不会执行任何代码。闭包字面量实际上在构造时不会执行闭包的任何代码,类似地,协程字面量在构造时不会执行协程内的任何代码
- 协程可以通过引用或移动捕获外部变量,这可以通过闭包开头的 move 关键字进行调整。像闭包一样,所有的协程都有一个由编译器推断的隐式环境。随着协程的进行,外部变量可以移动到协程中使用
- 协程字面量产生一个具有唯一类型的值,该类型实现了 std::ops::Coroutine trait。这允许通过 coroutine::resume 方法实际执行协程,也可以在返回类型中命名协程
- 像 Send 和 Sync 这样的特征会根据环境捕获的变量自动为协程实现。与闭包不同,协程还依赖于存在于挂起点之间的变量。这意味着尽管环境可能是 Send 或 Sync 的,但协程本身可能不是,因为内部变量存在于 not-
<font style="background-color:rgb(246, 247, 246);">Send</font>
或 not-<font style="background-color:rgb(246, 247, 246);">Sync</font>
的 yield 点上。请注意,协程不会自动实现像 Copy 或 Clone 这样的特性 - 每当一个协程被删除时,它将删除所有捕获的环境变量
协程作为状态机
在编译器中,协程当前被编译为状态机。每个 yield 表达式将对应于一个不同的状态,该状态存储了该暂停点上的所有活动变量。协程的恢复将根据当前状态进行调度,然后在内部执行,直到达到 yield 为止,此时协程中的所有状态都被保存并返回一个值。让我们看一个例子,看看这里发生了什么
#![feature(coroutines, coroutine_trait, stmt_expr_attributes)]
use std::ops::Coroutine;
use std::pin::Pin;
fn main() {
let ret = "foo";
let mut coroutine = #[coroutine] move || {
yield 1;
return ret
};
Pin::new(&mut coroutine).resume(());
Pin::new(&mut coroutine).resume(());
}
这个协程字面地将编译为类似于
#![feature(arbitrary_self_types, coroutine_trait)]
use std::ops::{Coroutine, CoroutineState};
use std::pin::Pin;
fn main() {
let ret = "foo";
let mut coroutine = {
enum __Coroutine {
Start(&'static str),
Yield1(&'static str),
Done,
}
impl Coroutine for __Coroutine {
type Yield = i32;
type Return = &'static str;
fn resume(mut self: Pin<&mut Self>, resume: ()) -> CoroutineState<i32, &'static str> {
use std::mem;
match mem::replace(&mut *self, __Coroutine::Done) {
__Coroutine::Start(s) => {
*self = __Coroutine::Yield1(s);
CoroutineState::Yielded(1)
}
__Coroutine::Yield1(s) => {
*self = __Coroutine::Done;
CoroutineState::Complete(s)
}
__Coroutine::Done => {
panic!("coroutine resumed after completion")
}
}
}
}
__Coroutine::Start(ret)
};
Pin::new(&mut coroutine).resume(());
Pin::new(&mut coroutine).resume(());
}
- 值得注意的是,在这里我们可以看到编译器生成了一个新的类型,在本例中是 __Coroutine。这种类型有许多状态(在这里表示为枚举),对应于协程的每个概念状态。开始时,我们关闭了外部变量 foo 然后这个变量也在 yield 点上,所以它被存储在两种状态
- 当协程开始运行时,它会立即 yield 1,但它会在执行之前保存状态,表明它已经到达 yield 点。再次恢复时,我们将执行 return ret,它将返回 Complete 的状态
- 这里我们还可以注意到,如果恢复完成的协程,Done 状态会立即发生错误,因为恢复已完成的协程是无效的。同样值得注意的是,这只是一个粗略的去糖,而不是编译器所做的规范性规范
总结一句话
生成器(Coroutine)包装 Future** —> Future 的 poll 调用生成器的 resume 方法**