關於協程和 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 對象也能夠作到這一點。

相關文章
相關標籤/搜索