章节目录

语言:一次 `resume` 只走到下一个边界

本节阅读量:

协程最容易被含混地描述为“函数可以暂停”。本章先不用这个说法,而是给三个表达式明确的求值规则。只要每次 resume 的结果可预测,后面的 lowering 才有可靠的目标。

coroutine 创建一个尚未运行的 generator

语法是:

1
(coroutine body)

求值它时,body 不执行。结果是一个以后可以交给 resume 的 generator 值。

因此:

1
2
3
4
5
6
7
(let touched 0
  (let g
    (coroutine
      (begin
        (set! touched 1)
        42))
    touched))

结果是 0,不是 1。创建 generator 只准备“第一步应该做什么”,并没有走出第一步。

coroutine_lazy.lang 用 if 把这条规则检查成最终的 42:

1
2
cd code/11_coroutines
./mini run examples/coroutine_lazy.lang

yield 产出一个值,并把它留给下一次恢复

语法是:

1
(yield expr)

当前这一次 resume 会先求 expr,然后暂停并返回:

1
(record 0 value)

其中 value 是 expr 的结果。下一次 resume 不会重新计算 expr;它从 yield 后面继续,并把这个已经得到的 value 作为整个 (yield expr) 的表达式值。

例如:

1
2
3
(coroutine
  (let x (yield 40)
    (+ x 2)))

第一次恢复时,yield 产出 40,整个 let 还没有建立 x。第二次恢复时,yield 的结果变成已有的 40,于是建立 x,再计算 (+ x 2) 得到 42。

这就是“本章不向 coroutine 发送值”的精确含义:恢复者没有提供新 argument,恢复后的 yield 也不凭空得到一个新值;它继续使用自己已经产出的值。

body 正常结束也会产生 record

若 body 没有再遇到 yield,本次 resume 返回:

1
(record 1 value)

1 表示 done,字段 1 是 body 的最终值。对主例子:

1
2
第一次 resume    (record 0 40)
第二次 resume    (record 1 42)

完成后再次 resume 不会回到 body 开头,也不会重复最后一次计算。它反复返回第一次完成时创建的那个 record。

1
./mini run examples/coroutine_done_repeat.lang

示例中的:

1
(eq? first second)

结果为真,说明两个 done 结果是同一个 record 对象,而不只是字段碰巧相同的两个 record。

resume 恢复一个 generator

语法是:

1
(resume expr)

它先按普通表达式的从左到右规则求 expr,然后调用其中保存的 generator step。这个顺序可观察:

1
./mini run examples/coroutine_resume_operand_order.lang

该示例在 resume 的 operand 中先执行 set!,再取得 g;最后结果是 42,说明 operand 没有被跳过或提前改写。

从 core AST 的角度,resume 会降成“以隐藏整数 0 调用一个一元 closure”。这个 0 不是 send 值,也不是用户可见的协议字段;它只是复用已有一元函数调用形状的占位 argument。

本章不为 coroutine 另设 runtime tag。因此 resume 能进行的动态检查与普通调用相同:operand 必须是 closure。下面会触发已有的 closure 错误:

1
./mini run examples/resume_integer_error.lang

输出包含:

1
error: function call expected a function

(coroutine ...) 产生的 closure 才保证遵守本节的二字段 record 协议;普通 closure 不是这个协议的一部分。

一个 generator 可以暂停多次

本章的 begin 固定为二元。嵌套 begin 足以写出多个暂停点:

1
2
3
4
5
6
(coroutine
  (begin
    (yield 10)
    (begin
      (yield 20)
      42)))

运行:

1
./mini run examples/coroutine_begin.lang

三次恢复依次经过:

1
2
3
(record 0 10)
(record 0 20)
(record 1 42)

示例忽略前两次暂停 record,读取第三次的完成值,因此输出 42。

不同 generator 各有自己的 state,不会互相抢走下一步:

1
./mini run examples/coroutine_interleaved.lang

它交错恢复两个 generator,仍然得到 42。这不是调度器;只是两个独立 closure 各自捕获了自己的 cell。

yield 的值先算完,状态随后才更新

yield 的 payload 也是普通表达式。它先完整求值,随后才安装下一步 closure 和返回暂停 record。

1
./mini run examples/coroutine_yield_order.lang

该程序的 payload 先把 value 改成 1,恢复后剩余 body 再把它改成 2。最终 42 证明两次副作用恰好分别发生在暂停前和恢复后。

若 payload 在求值时出错,则既没有 pause record,也不能错误地替换当前 state。这个顺序会在 lowering 一节具体实现。

重新进入正在执行的 coroutine 是错误

当一个 step 已经开始运行时,本章不允许它再次恢复自己。进入 step 的瞬间,lowering 临时把 state cell 写成整数 0;若 body 递归执行 (resume g),已有的 closure guard 会发现 0 不是函数并报告错误。

1
./mini run examples/coroutine_reentry_error.lang

输出包含:

1
error: function call expected a function

这条规则避免“同一个 step 同时运行两次”的难题。它不实现 generator 的重入、协作式 transfer 或独立机器栈;这些都超出本章边界。


11.0 把“下一步”做成一个值

上一节

11.2 边界:哪些位置可以 yield

下一节