smallyu的博客

smallyu的博客

马上订阅 smallyu的博客 RSS 更新: https://smallyu.net/atom.xml

continuation 教程: 用 yield 实现协程调度

2025年7月23日 12:13

这是一个 continuation 系列教程:

  1. continuation 教程:理解 CPS
  2. continuation 教程:用 yield 实现协程调度
  3. continuation 教程:用 call/cc 实现协程调度
  4. continuation 教程:用 shift/reset 实现协程调度
  5. continuation 教程:体验 Racket 语言
  6. continuation 教程:实现抢占式协程调度

任务队列

先来定义一个任务队列:

let ready = [];

然后定义一个执行函数:

function run(){  while (ready.length > 0)  {    const k = ready.shift()    k();  }}

从定义可以看出,任务队列中的元素都是函数,然后在运行函数 run 中,会依次执行队列中的函数。可以这样来使用我们的任务队列:

ready.push( () => console.log(1) );ready.push( () => console.log(2) );ready.push( () => console.log(3) );run();// 1// 2// 3

顺序执行

现在我们有这样两个 task 函数,把他们添加到任务队列后,会按照顺序执行并打印出结果,这很好理解,符合我们的直觉:

function taskA(){  console.log("task A0");  console.log("task A1");}function taskB(){  console.log("task B0");  console.log("task B1");}ready.push(taskA);ready.push(taskB);run();// task A0// task A1// task B0// task B1

现在的打印顺序是 A0 -> A1 -> B0 -> B1,有没有什么办法,可以改变打印顺序,变为 A0 -> B0 -> A1 -> B1 呢?这里的每一个打印语句都是一个 task,而我们关心的是 task 的执行顺序。

yield

yield 关键字的含义是,保存当前的执行环境,把当前任务放到队列最后面,然后去运行其他的任务。就像是在排队,yield 是一个非常讲礼貌的人,当轮到自己的时候,会自己跑去队伍最后面,继续排队。

很多语言都提供了 yield 关键字,我们现在要做的,是在不使用 yield 关键字的情况下,实现 yield 的语义。可以这样定义 yieldCPS 函数,这个函数干的事情,就相当于 yield 关键字:

function yieldCPS(k){  ready.push(k);              // 把当前步骤的执行环境存起来  const next = ready.shift(); // 去执行队列头部的其他任务  next();}yieldCPS( () => console.log("yield cps") );run();  // yield cps

yieldCPS 接受一个函数作为参数,如果我们想在 taskA 里使用 yield 语义来影响 A0A1 的执行顺序,可以这样写:

function taskAYield(yieldFn){  console.log("task yield A0");  yieldFn( () => console.log("task yield A1") );}ready.push( () => taskAYield(yieldCPS) );run();// task yield A0// task yield A1

但是你发现了,打印出来的顺序没有变,因为确实不应该变,A1 之后没有其他任务了,排队的时候,yield 定义的任务已经没有谦让的余地。

taskB 里也用上 yield 试试:

function taskBYield(yieldFn){  console.log("task yield B0");  yieldFn( () => console.log("task yield B1") );}

这个时候再把 taskAtaskB 放进任务队列,打印结果的顺序就有变化了:...

剩余内容已隐藏

查看完整文章以阅读更多