子程序,或者称为函数,在所有语言中都是层级调用的,比如 A 调用 B,B 在执行过程中又调用 C,C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕,显然子程序调用是通过栈实现的,一个线程就是执行一个子程序,子程序调用总是一个入口,一次返回,调用顺序是明确的;而协程的调用和子程序不同,协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
假设由协程执行,在执行 A 的过程中,可以随时中断,去执行 B,B 也可能在执行过程中中断再去执行 A,结果可能是:
1
2
x
y
3
z
但是在 A 中是没有调用 B 的,所以协程的调用比函数调用理解起来要难一些。看起来 A、B 的执行有点像多线程,但协程的特点在于是一个线程执行,和多线程比协程最大的优势就是协程极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态即可,所以执行效率比多线程高很多。
var q := new queue
coroutine produce
loop
while q is not full
create some new items
add the items to q
yield to consume
coroutine consume
loop
while q is not empty
remove some items from q
use the items
yield to produce
var q := new queue
generator produce
loop
while q is not full
create some new items
add the items to q
yield consume
generator consume
loop
while q is not empty
remove some items from q
use the items
yield produce
subroutine dispatcher
var d := new dictionary(generator → iterator)
d[produce] := start produce
d[consume] := start consume
var current := produce
loop
current := next d[current]
这是基于生成器实现的协程,我们看这里的 produce 与 consume 过程完全符合协程的概念,不难发现根据定义生成器本身就是协程。
var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shells');
console.log(r2.toString());
};
var g = gen();
var r1 = g.next();
r1.value(function (err, data) {
if (err) throw err;
var r2 = g.next(data);
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
});
});
仔细查看上面的代码,可以发现 Generator 函数的执行过程,其实是将同一个回调函数,反复传入 next 方法的 value 属性,这使得我们可以用递归来自动完成这个过程,下面就是一个基于 Thunk 函数的 Generator 执行器:
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
// ...
}
run(g);
关于协程和 ES6 中的 Generator
什么是协程?
进程和线程
众所周知,进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同,进程是 CPU 资源分配的最小单位,线程是 CPU 调度的最小单位。
其实协程(微线程,纤程,Coroutine)的概念很早就提出来了,可以认为是比线程更小的执行单元,但直到最近几年才在某些语言中得到广泛应用。
那么什么是协程呢?
子程序,或者称为函数,在所有语言中都是层级调用的,比如 A 调用 B,B 在执行过程中又调用 C,C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕,显然子程序调用是通过栈实现的,一个线程就是执行一个子程序,子程序调用总是一个入口,一次返回,调用顺序是明确的;而协程的调用和子程序不同,协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
我们用一个简单的例子来说明,比如现有程序 A 和 B:
假设由协程执行,在执行 A 的过程中,可以随时中断,去执行 B,B 也可能在执行过程中中断再去执行 A,结果可能是:
但是在 A 中是没有调用 B 的,所以协程的调用比函数调用理解起来要难一些。看起来 A、B 的执行有点像多线程,但协程的特点在于是一个线程执行,和多线程比协程最大的优势就是协程极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态即可,所以执行效率比多线程高很多。
Wiki 中的定义: Coroutine
解释协程时最常见的就是生产消费者模式:
这个例子中容易让人产生疑惑的一点就是 yield 的使用,它与我们通常所见的 yield 指令不同,因为我们常见的 yield 指令大都是基于生成器(Generator)这一概念的。
这是基于生成器实现的协程,我们看这里的 produce 与 consume 过程完全符合协程的概念,不难发现根据定义生成器本身就是协程。
什么是 Generator?
在本文我们使用 ES6 中的 Generators 特性来介绍生成器,它是 ES6 提供的一种异步编程解决方案,语法上首先可以把它理解成是一个状态机,封装多个内部状态,执行 Generator 函数会返回一个遍历器对象,也就是说 Generator 函数除状态机外,还是一个遍历器对象生成函数,返回的遍历器对象可以依次遍历 Generator 函数内部的每一个状态,先看一个简单的例子:
这段代码看起来很像一个函数,我们称之为生成器函数,它与普通函数有很多共同点,但是二者有如下区别:
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号,不同的是调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
当调用 quips() 生成器函数时发生什么?
每当生成器执行 yield 语句时,生成器的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置 etc.)被移出堆栈,然而生成器对象保留对这个堆栈结构的引用(备份),所以稍后调用 .next() 可以重新激活堆栈结构并且继续执行。当生成器运行时,它和调用者处于同一线程中,拥有确定的连续执行顺序,永不并发。
遍历器对象的 next 方法的运行逻辑如下:
生成器是迭代器!
迭代器是 ES6 中独立的内建类,同时也是语言的一个扩展点,通过实现 [Symbol.iterator]() 和 .next() 两个方法就可以创建自定义迭代器。
我们可以使用生成器实现上面循环中的 range 方法:
生成器是迭代器,所有的生成器都有内建 .next() 和 [Symbol.iterator]() 方法的实现,我们只需要编写循环部分的行为即可。
for...of 循环可以自动遍历 Generator 函数时生成的 Iterator 对象,且此时不再需要调用 next 方法。
上面代码使用 for...of 循环,依次显示 5 个 yield 表达式的值。这里需要注意,一旦 next 方法的返回对象的 done 属性为 true,for...of 循环就会中止,且不包含该返回对象,所以上面代码的 return 语句返回的6,不包括在 for...of 循环之中。
下面是一个利用 Generator 函数和 for...of 循环,实现斐波那契数列的例子:
使用 Generator 实现生产消费者模式
异步流程控制
ES6 诞生以前,异步编程的方法大概有下面四种:
想必大家都经历过同样的问题,在异步流程控制中会使用大量的回调函数,甚至出现多个回调函数嵌套导致的情况,代码不是纵向发展而是横向发展,很快就会乱成一团无法管理,因为多个异步操作形成强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改,这种情况就是我们常说的"回调函数地狱"。
Promise 对象就是为了解决这个问题而提出的,它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。然而,Promise 的最大问题就是代码冗余,原来的任务被 Promise 包装一下,不管什么操作一眼看去都是一堆 then,使得原来的语义变得很不清楚。
那么,有没有更好的写法呢?
哈哈这里有些明知故问,答案当然就是 Generator!Generator 函数是协程在 ES6 的实现,整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器,Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因,除此之外,它还有两个特性使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
上面代码中,第一个 next 方法的 value 属性,返回表达式 x + 2 的值3,第二个 next 方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量 y 接收,因此这一步的 value 属性返回的就是2(也就是变量 y 的值)。
上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try...catch 代码块捕获,这意味着出错的代码与处理错误的代码实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
Generator 函数的自动流程管理
Thunk 函数
函数的"传值调用"和“传名调用”一直以来都各有优劣(比如传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失),本文不多赘述,在这里需要提到的是:编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就叫做 Thunk 函数。
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 JavaScript 语言 Thunk 函数转换器:
你可能会问, Thunk 函数有什么用?回答是以前确实没什么用,但是 ES6 有了 Generator 函数,Thunk 函数现在可以用于 Generator 函数的自动流程管理。
首先 Generator 函数本身是可以自动执行的:
但是,这并不适合异步操作,如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行,这时 Thunk 函数就能派上用处,以读取文件为例,下面的 Generator 函数封装了两个异步操作:
上面代码中,yield 命令用于将程序的执行权移出 Generator 函数,那么就需要一种方法,将执行权再交还给 Generator 函数,这种方法就是 Thunk 函数,因为它可以在回调函数里,将执行权交还给 Generator 函数,为了便于理解,我们先看如何手动执行上面这个 Generator 函数:
仔细查看上面的代码,可以发现 Generator 函数的执行过程,其实是将同一个回调函数,反复传入 next 方法的 value 属性,这使得我们可以用递归来自动完成这个过程,下面就是一个基于 Thunk 函数的 Generator 执行器:
Thunk 函数并不是 Generator 函数自动执行的唯一方案,因为自动执行的关键是,必须有一种机制自动控制 Generator 函数的流程,接收和交还程序的执行权,回调函数可以做到这一点,Promise 对象也可以做到这一点。