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 methodfn 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");}12345
目前,协程的主要用例是 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 方法**
