CyanTalks
首发于CyanTalks
简单解释 Coroutine

简单解释 Coroutine

什么是 Coroutine?

Coroutine 可以被翻译为协程,它与 Subroutine(子程序)有什么区别呢?其实我们绝大多数接触到的函数也好,方法也好,都是 Subroutine 的一种,它的特点就是,子程序从函数入口开始,一直到 return 语句结束,也就是说,一旦子程序返回了,它这一阶段的所有使命就完成了。

而 Coroutine 是一种可以与其他 Coroutine 或 Subroutine 交叉运行的程序,这也是为什么 Coroutine 叫做 Coroutine 的原因了,一般来讲,一段 Coroutine 也会存在一个返回语句(通常叫做 yield),一旦执行到此语句,Coroutine 就会保存上下文并且将控制权交出(睡眠),此时调用这段 Coroutine 的 whatever-routine 就会『拿到 CPU 的控制权』,并且得到 yield 出来的变量,一旦时机成熟,刚才睡眠的 Coroutine 就又可以原地复苏,从刚才暂停下来的 yield 语句继续向下执行,直到 return 才彻底结束。

它与线程有什么关系?

我不知道是不是中文名字相近的缘故,很多人认为协程一定意义上意味着多线程。OK,我可以告诉大家,『Coroutine』 与 『Thread』 没有半毛钱的关系,它们的关系可以说就是:

明白了吗?即使你使用的语言是单线程的(如 JavaScript),依然没有半点妨碍到它去实现协程的支持。

事实上 Node.js 并不是真正意义上的单线程,它允许在 I/O 操作上使用多线程,但是 JavaScript 层的表现就是将多线程中处理结果通过 callbacks 传递回去继续执行,但是 JavaScript 的任何代码都是在单线程上执行的。

所以你别想通过 Coroutine 来实现执行效率的提高和异步的支持,只不过它可以让原本支持异步的写法变得更好写。(参见:tj / co


以下代码将以 JavaScript 呈现

Now in (Language)JS


初见 Coroutine

我们假设有这么一段计算 Fibonacii 的函数:

function fibonacii() {
  let i = 1;
  let j = 1;
  for (let k = 0; k < 10; k++) {
    console.log(i);
    console.log(j);
    i += j;
    j += i;
  }
}

显然,我们无法从这个 O(1) 的 Fibonacii 算法中一项一项地取出数值,所以必须限定一个迭代范围,不然就是死循环,然后通过副作用的方式将结果传向外界。

假设我们通过 yield 和 Generator 来重构这个函数:

function *fibonaciiUsingYield() {
  let i = 1;
  let j = 1;
  while (true) {
    yield i;
    yield j;
    i += j;
    j += i;
  }
}

此时调用 fibonaciiUsingYield 就会返回一个 Generator 对象, 你可以不断调用这个对象的 next 函数来控制迭代次数并从每次迭代中取出数组。Just like this:

const gen = fibonaciiUsingYield();
gen.next();

next 函数返回一个对象,包含了每次迭代的结果和一个可否继续迭代的 flag。

手动写一个 Coroutine

别看 Coroutine 这么炫酷,其实写起来十分简单。

首先,执行到 yield 语句时,当前正在执行的 Coroutine 函数是肯定会返回的,不然根本不能将结果传出去并让调用者获得控制权。OK,那么如果我想让 Coroutine 继续,那肯定还是要重新调用刚才的 Coroutine 函数的,此时由于我们要恢复刚才执行的状态,就一定需要完整的本地变量(上下文信息)和刚才执行到的地方(行数)。

C 语言中很好实现跳转,goto 语句完事。在 JavaScript 中我们可以通过 switch case 的方式达到相同的目的。

接下来就是保存上下文了,C 语言中,无疑,没有闭包这样的语法,简单点的方式就是将所有局部变量用 static 修饰,或者你写一个结构体,从参数中传进来都可以。

JavaScript 中就很好实现了,由于有闭包,我们完全可以将执行体放到一个闭包里,然后在闭包外声明所有所需的局部变量和状态信息,让闭包自动捕获它们。所以一个简单的实现方式就出来了:

function genFibonaciiCoroutine() {
  let i = 1;
  let j = 1;
  let state = 1;
  return function () {
    let retVal;
    switch (state) {
      case 0:
        i += j;
        j += i;
        state = 1;
      case 1:
        retVal = i;
        state = 2;
        break;
      case 2:
        retVal = j;
        state = 0;
        break;
    }
    return retVal;
  }
}

调用 genFibonaciiCoroutine 就会返回一个绑定好局部变量的闭包,每次调用闭包,都能返回一个结果。

其实观察代码就能发现,所有的逻辑基本没有变化,顺序有些变化,因为每次调用函数都要返回值的,所以我将不会返回值的那个 phase 提前了。这样一来,每次进入函数时,根据 state 跳转到相应的『行』,即将 return 时,将 state 移至下一次唤醒时应该位于的值。整个重写过程其实就是人肉 CPS。


最后,大家可以自己尝试给这个『Coroutine』加一个终止条件,相信很容易实现。

编辑于 2017-03-24

文章被以下专栏收录