smallyu的博客

smallyu的博客

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

continuation教程: 实现抢占式协程调度

2025年7月23日 12:17

这是一个 continuation 系列教程:

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

你也许注意到了,我们前面用 yield 关键在来实现两个任务的交替打印,似乎和我们平时使用协程的感受不一样,比如 Go 语言的协程往往用来后台启动一个 Server 服务之类;跟我们平时使用 node.js 的感觉也不太一样。

我们用 yield 关键字,以及用 call/cc 或者 shift/reset 关键字代替 yield 实现控制流,这种显式管理流的模式属于协作式协程(cooperative)。

Go 语言一键启动后台协程、交给 runtime 管理控制流的模式,属于抢占式协程(preemptive)。

node.js 语言常见的异步调用、Promise 关键字之类,属于异步协程。

他们都属于协程,但是给用户的感受不一样,尤其是抢占式协程,在用户层面感知不到协程调度器的工作,但是抢占式协程的内部实现仍然要依赖于 continuation 概念。

构建 CPS

我们基于之前 shift/reset 版本的代码来进行实验和改进。首先这是用 shift/reset 模拟 yield 效果的完整代码,程序会交替执行两个任务:

let ready = [];function run(){  while (ready.length > 0)  {    const k = ready.shift()    k();  }}function reset(thunk){  try  {    thunk(x => x);  }  catch (f)  {    f( v => ready.push(v) );  }}function shift(f){  throw f;}function spawn(thunk){  ready.push(thunk);}function taskA(){  reset(    k =>    {      shift(        k1 =>        {          console.log("task A0");          k1( () => console.log("task A1"));        }      );    }  );}function taskB(){  reset(    k =>    {      shift(        k1 =>        {          console.log("task B0");          k1( () => console.log("task B1"));        }      );    }  );}spawn(taskA);spawn(taskB);run();// task A0// task B0// task A1// task B1

你肯定注意到 shift 内部是嵌套 CPS 的写法,现在只是打印了 A0 和 A1。那么假如我想新增加 100 个步骤进去,难道要手动写 100 个嵌套的 CPS 函数吗?

所以我们写一个工具函数来生成 CPS 函数。我们先看一下,假如在 taskA 里增加一个步骤 A2,应该怎么写:

function taskA(){  reset(    k =>    {      shift(        k1 =>        {          console.log("task A0");          k1(             () =>             {              console.log("task A1");              k1( () => console.log("task A2"));            }          );        }      );    }  );}

可以看到,关键在于 shift 函数中对 k1 的重复调用,那么我们写这样一个函数,这个函数返回嵌套指定多次...

剩余内容已隐藏

查看完整文章以阅读更多