众所周知,进程和线程都是一个时间段的描述,是CPU工做时间段的描述,不过是颗粒大小不一样,进程是 CPU 资源分配的最小单位,线程是 CPU 调度的最小单位。shell
其实协程(微线程,纤程,Coroutine)的概念很早就提出来了,能够认为是比线程更小的执行单元,但直到最近几年才在某些语言中获得普遍应用。编程
子程序,或者称为函数,在全部语言中都是层级调用的,好比 A 调用 B,B 在执行过程当中又调用 C,C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕,显然子程序调用是经过栈实现的,一个线程就是执行一个子程序,子程序调用老是一个入口,一次返回,调用顺序是明确的;而协程的调用和子程序不一样,协程看上去也是子程序,但执行过程当中,在子程序内部可中断,而后转而执行别的子程序,在适当的时候再返回来接着执行。多线程
咱们用一个简单的例子来讲明,好比现有程序 A 和 B:并发
def A(): print '1' print '2' print '3' def B(): print 'x' print 'y' print 'z'
假设由协程执行,在执行 A 的过程当中,能够随时中断,去执行 B,B 也可能在执行过程当中中断再去执行 A,结果多是:app
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
这个例子中容易让人产生疑惑的一点就是 yield 的使用,它与咱们一般所见的 yield 指令不一样,由于咱们常见的 yield 指令大都是基于生成器(Generator)这一律念的。oop
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 过程彻底符合协程的概念,不难发现根据定义生成器自己就是协程。性能
“子程序就是协程的一种特例。” —— Donald Knuth
在本文咱们使用 ES6 中的 Generators 特性来介绍生成器,它是 ES6 提供的一种异步编程解决方案,语法上首先能够把它理解成是一个状态机,封装多个内部状态,执行 Generator 函数会返回一个遍历器对象,也就是说 Generator 函数除状态机外,仍是一个遍历器对象生成函数,返回的遍历器对象能够依次遍历 Generator 函数内部的每个状态,先看一个简单的例子:
function* quips(name) { yield "你好 " + name + "!"; yield "但愿你能喜欢这篇介绍ES6的译文"; if (name.startsWith("X")) { yield "你的名字 " + name + " 首字母是X,这很酷!"; } yield "咱们下次再见!"; }
这段代码看起来很像一个函数,咱们称之为生成器函数,它与普通函数有不少共同点,可是两者有以下区别:
Generator 函数的调用方法与普通函数同样,也是在函数名后面加上一对圆括号,不一样的是调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
> var iter = quips("jorendorff"); [object Generator] > iter.next() { value: "你好 jorendorff!", done: false } > iter.next() { value: "但愿你能喜欢这篇介绍ES6的译文", done: false } > iter.next() { value: "咱们下次再见!", done: false } > iter.next() { value: undefined, done: true }
每当生成器执行 yield 语句时,生成器的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置 etc.)被移出堆栈,然而生成器对象保留对这个堆栈结构的引用(备份),因此稍后调用 .next() 能够从新激活堆栈结构而且继续执行。当生成器运行时,它和调用者处于同一线程中,拥有肯定的连续执行顺序,永不并发。
遍历器对象的 next 方法的运行逻辑以下:
迭代器是 ES6 中独立的内建类,同时也是语言的一个扩展点,经过实现 [Symbol.iterator]() 和 .next() 两个方法就能够建立自定义迭代器。
// 应该弹出三次 "ding" for (var value of range(0, 3)) { alert("Ding! at floor #" + value); }
咱们可使用生成器实现上面循环中的 range 方法:
function* range(start, stop) { for (var i = start; i < stop; i++) yield i; }
生成器是迭代器,全部的生成器都有内建 .next() 和 [Symbol.iterator]() 方法的实现,咱们只须要编写循环部分的行为便可。
for...of 循环能够自动遍历 Generator 函数时生成的 Iterator 对象,且此时再也不须要调用 next 方法。
function* foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5
上面代码使用 for...of 循环,依次显示 5 个 yield 表达式的值。这里须要注意,一旦 next 方法的返回对象的 done 属性为 true,for...of 循环就会停止,且不包含该返回对象,因此上面代码的 return 语句返回的6,不包括在 for...of 循环之中。
下面是一个利用 Generator 函数和 for...of 循环,实现斐波那契数列的例子:
function* fibonacci() { let [prev, curr] = [0, 1]; for (;;) { [prev, curr] = [curr, prev + curr]; yield curr; } } for (let n of fibonacci()) { if (n > 1000) break; console.log(n); }
除了 for...of 循环之外,扩展运算符(...)、解构赋值和 Array.from 方法内部调用的,都是遍历器接口,这意味着它们均可以将 Generator 函数返回的 Iterator 对象做为参数。
function producer(c) { c.next(); let n = 0; while (n < 5) { n++; console.log(`[PRODUCER] Producing ${n}`); const { value: r } = c.next(n); console.log(`[PRODUCER] Consumer return: ${r}`); } c.return(); } function* consumer() { let r = ''; while (true) { const n = yield r; if (!n) return; console.log(`[CONSUMER] Consuming ${n}`); r = '200 OK'; } } const c = consumer(); producer(c);
[PRODUCER] Producing 1 [CONSUMER] Consuming 1 [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 2 [CONSUMER] Consuming 2 [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 3 [CONSUMER] Consuming 3 [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 4 [CONSUMER] Consuming 4 [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 5 [CONSUMER] Consuming 5 [PRODUCER] Consumer return: 200 OK [Finished in 0.1s]
ES6 诞生之前,异步编程的方法大概有下面四种:
想必你们都经历过一样的问题,在异步流程控制中会使用大量的回调函数,甚至出现多个回调函数嵌套致使的状况,代码不是纵向发展而是横向发展,很快就会乱成一团没法管理,由于多个异步操做造成强耦合,只要有一个操做须要修改,它的上层回调函数和下层回调函数,可能都要跟着修改,这种状况就是咱们常说的"回调函数地狱"。
Promise 对象就是为了解决这个问题而提出的,它不是新的语法功能,而是一种新的写法,容许将回调函数的嵌套,改为链式调用。然而,Promise 的最大问题就是代码冗余,原来的任务被 Promise 包装一下,无论什么操做一眼看去都是一堆 then,使得原来的语义变得很不清楚。
哈哈这里有些明知故问,答案固然就是 Generator!Generator 函数是协程在 ES6 的实现,整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器,Generator 函数能够暂停执行和恢复执行,这是它能封装异步任务的根本缘由,除此以外,它还有两个特性使它能够做为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true }
上面代码中,第一个 next 方法的 value 属性,返回表达式 x + 2 的值3,第二个 next 方法带有参数2,这个参数能够传入 Generator 函数,做为上个阶段异步任务的返回结果,被函数体内的变量 y 接收,所以这一步的 value 属性返回的就是2(也就是变量 y 的值)。
function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; } var g = gen(1); g.next(); g.throw('出错了'); // 出错了
上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,能够被函数体内的 try...catch 代码块捕获,这意味着出错的代码与处理错误的代码实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
函数的"传值调用"和“传名调用”一直以来都各有优劣(好比传值调用比较简单,可是对参数求值的时候,实际上还没用到这个参数,有可能形成性能损失),本文很少赘述,在这里须要提到的是:编译器的“传名调用”实现,每每是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就叫作 Thunk 函数。
function f(m) { return m * 2; } f(x + 5); // 等同于 var thunk = function () { return x + 5; }; function f(thunk) { return thunk() * 2; }
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 JavaScript 语言 Thunk 函数转换器:
// ES5 版本 var Thunk = function(fn){ return function (){ var args = Array.prototype.slice.call(arguments); return function (callback){ args.push(callback); return fn.apply(this, args); } }; }; // ES6 版本 const Thunk = function(fn) { return function (...args) { return function (callback) { return fn.call(this, ...args, callback); } }; };
你可能会问, Thunk 函数有什么用?回答是之前确实没什么用,可是 ES6 有了 Generator 函数,Thunk 函数如今能够用于 Generator 函数的自动流程管理。
首先 Generator 函数自己是能够自动执行的:
function* gen() { // ... } var g = gen(); var res = g.next(); while(!res.done){ console.log(res.value); res = g.next(); }
可是,这并不适合异步操做,若是必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行,这时 Thunk 函数就能派上用处,以读取文件为例,下面的 Generator 函数封装了两个异步操做:
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()); };
上面代码中,yield 命令用于将程序的执行权移出 Generator 函数,那么就须要一种方法,将执行权再交还给 Generator 函数,这种方法就是 Thunk 函数,由于它能够在回调函数里,将执行权交还给 Generator 函数,为了便于理解,咱们先看如何手动执行上面这个 Generator 函数:
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);
Thunk 函数并非 Generator 函数自动执行的惟一方案,由于自动执行的关键是,必须有一种机制自动控制 Generator 函数的流程,接收和交还程序的执行权,回调函数能够作到这一点,Promise 对象也能够作到这一点。