摘要:關于協程和中的什么是協程進程和線程眾所周知,進程和線程都是一個時間段的描述,是工作時間段的描述,不過是顆粒大小不同,進程是資源分配的最小單位,線程是調度的最小單位。子程序就是協程的一種特例。
關于協程和 ES6 中的 Generator 什么是協程? 進程和線程
眾所周知,進程和線程都是一個時間段的描述,是CPU工作時間段的描述,不過是顆粒大小不同,進程是 CPU 資源分配的最小單位,線程是 CPU 調度的最小單位。
其實協程(微線程,纖程,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,結果可能是:
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)這一概念的。
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 方法的運行邏輯如下:
遇到 yield 表達式,就暫停執行后面的操作,并將緊跟在 yield 后面的那個表達式的值,作為返回的對象的 value 屬性值
下一次調用 next 方法時,再繼續往下執行,直到遇到下一個 yield 表達式
如果沒有再遇到新的 yield 表達式,就一直運行到函數結束,直到 return 語句為止,并將 return 語句后面的表達式的值,作為返回的對象的 value 屬性值
如果該函數沒有 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 誕生以前,異步編程的方法大概有下面四種:
回調函數
事件監聽
發布/訂閱
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 對象也可以做到這一點。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94428.html
摘要:線程擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程亦由操作系統調度標準線程是的。以及鳥哥翻譯的這篇詳細文檔我就以他實現的協程多任務調度為基礎做一下例子說明并說一下關于我在阻塞方面所做的一些思考。 進程、線程、協程 關于進程、線程、協程,有非常詳細和豐富的博客或者學習資源,我不在此做贅述,我大致在此介紹一下這幾個東西。 進程擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,進程由操作系...
摘要:傳統的異步方法回調函數事件監聽發布訂閱之前寫過一篇關于的文章,里邊寫過關于異步的一些概念。內部函數就是的回調函數,函數首先把函數的指針指向函數的下一步方法,如果沒有,就把函數傳給函數屬性,否則直接退出。 Generator函數與異步編程 因為js是單線程語言,所以需要異步編程的存在,要不效率太低會卡死。 傳統的異步方法 回調函數 事件監聽 發布/訂閱 Promise 之前寫過一篇關...
摘要:協程,又稱微線程,纖程。最大的優勢就是協程極高的執行效率。生產者產出第條數據返回更新值更新消費者正在調用第條數據查看當前進行的線程函數中有,返回值為生成器庫實現協程通過提供了對協程的基本支持,但是不完全。 協程,又稱微線程,纖程。英文名Coroutine協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然后轉而執行別的子程序,在適當的時候再返回來接著執行。 最大的優勢就是協程極高...
閱讀 2078·2021-11-23 10:13
閱讀 2788·2021-11-09 09:47
閱讀 2737·2021-09-22 15:08
閱讀 3312·2021-09-03 10:46
閱讀 2230·2019-08-30 15:54
閱讀 909·2019-08-28 18:09
閱讀 2429·2019-08-26 18:26
閱讀 2341·2019-08-26 13:48