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)
  1. pub trait Generator<R = ()> {
  2. type Yield;
  3. type Return;
  4. // Required method
  5. fn resume(
  6. self: Pin<&mut Self>,
  7. arg: R
  8. ) -> GeneratorState<Self::Yield, Self::Return>;
  9. }
  • 由内置生成器类型实现的 trait
  • 生成器,通常也称为协程,目前是 Rust 中的一个实验性语言特性。 RFC 2033 中添加的生成器目前主要用于为 async/await 语法提供构建块,但可能会扩展为迭代器和其他原语提供符合人体工程学的定义
  • 生成器的语法和语义不稳定,将需要进一步的 RFC 来稳定。但是,此时的语法类似于闭包
  1. #![feature(generators, generator_trait)]
  2. use std::ops::{Generator, GeneratorState};
  3. use std::pin::Pin;
  4. fn main() {
  5. let mut generator = || {
  6. yield 1;
  7. "foo"
  8. };
  9. match Pin::new(&mut generator).resume(()) {
  10. GeneratorState::Yielded(1) => {}
  11. _ => panic!("unexpected return from resume"),
  12. }
  13. match Pin::new(&mut generator).resume(()) {
  14. GeneratorState::Complete("foo") => {}
  15. _ => panic!("unexpected return from resume"),
  16. }
  17. }

标准库将 Gnerator 改名

后来 Rust 官方将上面的生成器重新改名Coroutine

https://doc.rust-lang.org/unstable-book/language-features/coroutines.html

此功能的跟踪 issue 是:#43122

  • Rust 中的协程(**coroutines**特性允许你定义 协程 协程字面量。协程是一种“可恢复函数”,它在语法上类似于闭包,但在编译器中被编译为完全不同的语义。协程的主要特性是它可以在执行期间挂起,然后在稍后的时期恢复执行。协程使用 yield 关键字来“返回”,然后调用者可以在 yield 关键字之后恢复协程以恢复执行
  • 目前,协程是编译器中的一个额外的不稳定特性。在 RFC 2033 中添加的它们现在主要是作为信息/约束收集阶段。这样做的目的是,在实际稳定之前,可以在 nightly 编译器上进行实验。还需要进一步的 RFC 来稳定协程,可能会对整体设计进行至少一些小的调整
  1. #![feature(coroutines, coroutine_trait, stmt_expr_attributes)]
  2. use std::ops::{Coroutine, CoroutineState};
  3. use std::pin::Pin;
  4. fn main() {
  5. let mut coroutine = #[coroutine] || {
  6. yield 1;
  7. return "foo"
  8. };
  9. match Pin::new(&mut coroutine).resume(()) {
  10. CoroutineState::Yielded(1) => {}
  11. _ => panic!("unexpected value from resume"),
  12. }
  13. match Pin::new(&mut coroutine).resume(()) {
  14. CoroutineState::Complete("foo") => {}
  15. _ => panic!("unexpected value from resume"),
  16. }
  17. }

协程类似闭包的字面量,用 #[coroutine] 注释,可以包含 yield 语句。yield 语句接受一个可选的表达式作为参数,将其输出到协程之外。所有的协程字面量都在 std::ops 模块中实现了协程特征。协程特征有一个主要方法 resume,它在前一个暂停点恢复协程的执行。协程控制流的一个例子是以下示例按顺序打印所有数字

  1. #![feature(coroutines, coroutine_trait, stmt_expr_attributes)]
  2. use std::ops::Coroutine;
  3. use std::pin::Pin;
  4. fn main() {
  5. let mut coroutine = #[coroutine] || {
  6. println!("2");
  7. yield;
  8. println!("4");
  9. };
  10. println!("1");
  11. Pin::new(&mut coroutine).resume(());
  12. println!("3");
  13. Pin::new(&mut coroutine).resume(());
  14. println!("5");
  15. }
  16. 1
  17. 2
  18. 3
  19. 4
  20. 5

目前,协程的主要用例是 async/awaitgen 语法的实现原语,但协程在未来可能会扩展到其他原语。关于设计和使用的反馈总是很受欢迎的

The Coroutine trait

std::ops 中的协程特征目前看起来是这样的

  1. pub trait Coroutine<R = ()> {
  2. type Yield;
  3. type Return;
  4. fn resume(self: Pin<&mut Self>, resume: R) -> CoroutineState<Self::Yield, Self::Return>;
  5. }
  • Coroutine::Yield 类型是 Yield 语句可以生成的值的类型
    • 该关联类型对应于 yield 表达式和协程每次执行 yield 时允许返回的值。例如,作为协程的迭代器可能有这种类型 T,正在迭代的类型
  • Coroutine::Return 类型是协程的返回类型。这通常是协程定义中的最后一个表达式,或者协程中传递给返回的任何值
    • 这对应于从协程返回的类型,要么用 return 语句返回,要么隐式地作为协程字面量的最后一个表达式。例如,futures 会使用Result,因为它代表一个完成的 future
  • resume 函数是执行协程本身的入口点
    • 该函数将恢复协程的执行,如果还没有开始执行,则开始执行。这个调用将返回到协程的最后一个暂停点,从最新的 yield 恢复执行。协程将继续执行,直到它 yield 或 return,此时该函数将返回
    • 返回值:这个函数返回的 CoroutineState 枚举表示协程返回时处于什么状态。如果生成的变体被返回,那么协程已经到达一个暂停点,并且生成了一个值。处于这种状态的协程可以在稍后恢复。如果返回 Complete,则协程已经完全完成了所提供的值。协程再次恢复是无效的
    • Panics:如果在之前返回<font style="color:rgb(0, 0, 0);">Complete</font>之后调用该函数,则可能会出现错误。虽然语言中的协程字面量在完成后恢复时保证会出现panic,但对于协程 trait 的所有实现来说,这并不是保证

resume 的返回值 CoroutineState 如下所示

  1. pub enum CoroutineState<Y, R> {
  2. Yielded(Y),
  3. Complete(R),
  4. }
  • 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 为止,此时协程中的所有状态都被保存并返回一个值。让我们看一个例子,看看这里发生了什么

  1. #![feature(coroutines, coroutine_trait, stmt_expr_attributes)]
  2. use std::ops::Coroutine;
  3. use std::pin::Pin;
  4. fn main() {
  5. let ret = "foo";
  6. let mut coroutine = #[coroutine] move || {
  7. yield 1;
  8. return ret
  9. };
  10. Pin::new(&mut coroutine).resume(());
  11. Pin::new(&mut coroutine).resume(());
  12. }

这个协程字面地将编译为类似于

  1. #![feature(arbitrary_self_types, coroutine_trait)]
  2. use std::ops::{Coroutine, CoroutineState};
  3. use std::pin::Pin;
  4. fn main() {
  5. let ret = "foo";
  6. let mut coroutine = {
  7. enum __Coroutine {
  8. Start(&'static str),
  9. Yield1(&'static str),
  10. Done,
  11. }
  12. impl Coroutine for __Coroutine {
  13. type Yield = i32;
  14. type Return = &'static str;
  15. fn resume(mut self: Pin<&mut Self>, resume: ()) -> CoroutineState<i32, &'static str> {
  16. use std::mem;
  17. match mem::replace(&mut *self, __Coroutine::Done) {
  18. __Coroutine::Start(s) => {
  19. *self = __Coroutine::Yield1(s);
  20. CoroutineState::Yielded(1)
  21. }
  22. __Coroutine::Yield1(s) => {
  23. *self = __Coroutine::Done;
  24. CoroutineState::Complete(s)
  25. }
  26. __Coroutine::Done => {
  27. panic!("coroutine resumed after completion")
  28. }
  29. }
  30. }
  31. }
  32. __Coroutine::Start(ret)
  33. };
  34. Pin::new(&mut coroutine).resume(());
  35. Pin::new(&mut coroutine).resume(());
  36. }
  • 值得注意的是,在这里我们可以看到编译器生成了一个新的类型,在本例中是 __Coroutine。这种类型有许多状态(在这里表示为枚举),对应于协程的每个概念状态。开始时,我们关闭了外部变量 foo 然后这个变量也在 yield 点上,所以它被存储在两种状态
  • 当协程开始运行时,它会立即 yield 1,但它会在执行之前保存状态,表明它已经到达 yield 点。再次恢复时,我们将执行 return ret,它将返回 Complete 的状态
  • 这里我们还可以注意到,如果恢复完成的协程,Done 状态会立即发生错误,因为恢复已完成的协程是无效的。同样值得注意的是,这只是一个粗略的去糖,而不是编译器所做的规范性规范

总结一句话

生成器(Coroutine)包装 Future** —> Future 的 poll 调用生成器的 resume 方法**