continuation教程: 实现抢占式协程调度
这是一个 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 的重复调用,那么我们写这样一个函数,这个函数返回嵌套指定多次...
剩余内容已隐藏