衆所周知,進程和線程都是一個時間段的描述,是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 對象也能夠作到這一點。