关于协程和 ES6 中的 Generator

关于协程和 ES6 中的 Generator

什么是协程?

进程和线程

众所周知,进程线程都是一个时间段的描述,是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 的执行有点像多线程,但协程的特色在于是一个线程执行,和多线程比协程最大的优点就是协程极高的执行效率,由于子程序切换不是线程切换,而是由程序自身控制,所以没有线程切换的开销,和多线程比,线程数量越多,协程的性能优点就越明显;第二大优点就是不须要多线程的锁机制,由于只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只须要判断状态便可,因此执行效率比多线程高不少。异步

Wiki 中的定义: Coroutine异步编程

协程是一种程序组件,是由子例程(过程、函数、例程、方法、子程序)的概念泛化而来的,子例程只有一个入口点且只返回一次,而协程容许多个入口点,能够在指定位置挂起和恢复执行。
  • 协程的本地数据在后续调用中始终保持
  • 协程在控制离开时暂停执行,当控制再次进入时只能从离开的位置继续执行

解释协程时最多见的就是生产消费者模式:函数

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

什么是 Generator?

在本文咱们使用 ES6 中的 Generators 特性来介绍生成器,它是 ES6 提供的一种异步编程解决方案,语法上首先能够把它理解成是一个状态机,封装多个内部状态,执行 Generator 函数会返回一个遍历器对象,也就是说 Generator 函数除状态机外,仍是一个遍历器对象生成函数,返回的遍历器对象能够依次遍历 Generator 函数内部的每个状态,先看一个简单的例子:

function* quips(name) {
  yield "你好 " + name + "!";
  yield "但愿你能喜欢这篇介绍ES6的译文";
  if (name.startsWith("X")) {
    yield "你的名字 " + name + "  首字母是X,这很酷!";
  }
  yield "咱们下次再见!";
}

这段代码看起来很像一个函数,咱们称之为生成器函数,它与普通函数有不少共同点,可是两者有以下区别:

  • 普通函数使用 function 声明,而生成器函数使用 function* 声明
  • 在生成器函数内部,有一种相似 return 的语法即关键字 yield,两者的区别是普通函数只能够 return 一次,而生成器函数能够 yield 屡次,在生成器函数的执行过程当中,遇到 yield 表达式当即暂停,而且后续可恢复执行状态

Generator 函数的调用方法与普通函数同样,也是在函数名后面加上一对圆括号,不一样的是调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象

当调用 quips() 生成器函数时发生什么?

> 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 方法的运行逻辑以下:

  1. 遇到 yield 表达式,就暂停执行后面的操做,并将紧跟在 yield 后面的那个表达式的值,做为返回的对象的 value 属性值
  2. 下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式
  3. 若是没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,做为返回的对象的 value 属性值
  4. 若是该函数没有 return 语句,则返回的对象的 value 属性值为 undefined

生成器是迭代器!

迭代器是 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 对象做为参数。

使用 Generator 实现生产消费者模式

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 诞生之前,异步编程的方法大概有下面四种:

  1. 回调函数
  2. 事件监听
  3. 发布/订阅
  4. Promise 对象

想必你们都经历过一样的问题,在异步流程控制中会使用大量的回调函数,甚至出现多个回调函数嵌套致使的状况,代码不是纵向发展而是横向发展,很快就会乱成一团没法管理,由于多个异步操做造成强耦合,只要有一个操做须要修改,它的上层回调函数和下层回调函数,可能都要跟着修改,这种状况就是咱们常说的"回调函数地狱"。

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 代码块捕获,这意味着出错的代码与处理错误的代码实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

Generator 函数的自动流程管理

Thunk 函数

函数的"传值调用"和“传名调用”一直以来都各有优劣(好比传值调用比较简单,可是对参数求值的时候,实际上还没用到这个参数,有可能形成性能损失),本文很少赘述,在这里须要提到的是:编译器的“传名调用”实现,每每是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就叫作 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 对象也能够作到这一点。

相关文章
相关标签/搜索