摘要:關于協(xié)程和中的什么是協(xié)程進程和線程眾所周知,進程和線程都是一個時間段的描述,是工作時間段的描述,不過是顆粒大小不同,進程是資源分配的最小單位,線程是調(diào)度的最小單位。子程序就是協(xié)程的一種特例。
關于協(xié)程和 ES6 中的 Generator 什么是協(xié)程? 進程和線程
眾所周知,進程和線程都是一個時間段的描述,是CPU工作時間段的描述,不過是顆粒大小不同,進程是 CPU 資源分配的最小單位,線程是 CPU 調(diào)度的最小單位。
其實協(xié)程(微線程,纖程,Coroutine)的概念很早就提出來了,可以認為是比線程更小的執(zhí)行單元,但直到最近幾年才在某些語言中得到廣泛應用。
那么什么是協(xié)程呢?子程序,或者稱為函數(shù),在所有語言中都是層級調(diào)用的,比如 A 調(diào)用 B,B 在執(zhí)行過程中又調(diào)用 C,C 執(zhí)行完畢返回,B 執(zhí)行完畢返回,最后是 A 執(zhí)行完畢,顯然子程序調(diào)用是通過棧實現(xiàn)的,一個線程就是執(zhí)行一個子程序,子程序調(diào)用總是一個入口,一次返回,調(diào)用順序是明確的;而協(xié)程的調(diào)用和子程序不同,協(xié)程看上去也是子程序,但執(zhí)行過程中,在子程序內(nèi)部可中斷,然后轉(zhuǎn)而執(zhí)行別的子程序,在適當?shù)臅r候再返回來接著執(zhí)行。
我們用一個簡單的例子來說明,比如現(xiàn)有程序 A 和 B:
def A(): print "1" print "2" print "3" def B(): print "x" print "y" print "z"
假設由協(xié)程執(zhí)行,在執(zhí)行 A 的過程中,可以隨時中斷,去執(zhí)行 B,B 也可能在執(zhí)行過程中中斷再去執(zhí)行 A,結(jié)果可能是:
1 2 x y 3 z
但是在 A 中是沒有調(diào)用 B 的,所以協(xié)程的調(diào)用比函數(shù)調(diào)用理解起來要難一些。看起來 A、B 的執(zhí)行有點像多線程,但協(xié)程的特點在于是一個線程執(zhí)行,和多線程比協(xié)程最大的優(yōu)勢就是協(xié)程極高的執(zhí)行效率,因為子程序切換不是線程切換,而是由程序自身控制,因此沒有線程切換的開銷,和多線程比,線程數(shù)量越多,協(xié)程的性能優(yōu)勢就越明顯;第二大優(yōu)勢就是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協(xié)程中控制共享資源不加鎖,只需要判斷狀態(tài)即可,所以執(zhí)行效率比多線程高很多。
Wiki 中的定義: Coroutine
協(xié)程是一種程序組件,是由子例程(過程、函數(shù)、例程、方法、子程序)的概念泛化而來的,子例程只有一個入口點且只返回一次,而協(xié)程允許多個入口點,可以在指定位置掛起和恢復執(zhí)行。
協(xié)程的本地數(shù)據(jù)在后續(xù)調(diào)用中始終保持
協(xié)程在控制離開時暫停執(zhí)行,當控制再次進入時只能從離開的位置繼續(xù)執(zhí)行
解釋協(xié)程時最常見的就是生產(chǎn)消費者模式:
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
這個例子中容易讓人產(chǎn)生疑惑的一點就是 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]
這是基于生成器實現(xiàn)的協(xié)程,我們看這里的 produce 與 consume 過程完全符合協(xié)程的概念,不難發(fā)現(xiàn)根據(jù)定義生成器本身就是協(xié)程。
“子程序就是協(xié)程的一種特例?!?—— Donald Knuth什么是 Generator?
在本文我們使用 ES6 中的 Generators 特性來介紹生成器,它是 ES6 提供的一種異步編程解決方案,語法上首先可以把它理解成是一個狀態(tài)機,封裝多個內(nèi)部狀態(tài),執(zhí)行 Generator 函數(shù)會返回一個遍歷器對象,也就是說 Generator 函數(shù)除狀態(tài)機外,還是一個遍歷器對象生成函數(shù),返回的遍歷器對象可以依次遍歷 Generator 函數(shù)內(nèi)部的每一個狀態(tài),先看一個簡單的例子:
function* quips(name) { yield "你好 " + name + "!"; yield "希望你能喜歡這篇介紹ES6的譯文"; if (name.startsWith("X")) { yield "你的名字 " + name + " 首字母是X,這很酷!"; } yield "我們下次再見!"; }
這段代碼看起來很像一個函數(shù),我們稱之為生成器函數(shù),它與普通函數(shù)有很多共同點,但是二者有如下區(qū)別:
普通函數(shù)使用 function 聲明,而生成器函數(shù)使用 function* 聲明
在生成器函數(shù)內(nèi)部,有一種類似 return 的語法即關鍵字 yield,二者的區(qū)別是普通函數(shù)只可以 return 一次,而生成器函數(shù)可以 yield 多次,在生成器函數(shù)的執(zhí)行過程中,遇到 yield 表達式立即暫停,并且后續(xù)可恢復執(zhí)行狀態(tài)
Generator 函數(shù)的調(diào)用方法與普通函數(shù)一樣,也是在函數(shù)名后面加上一對圓括號,不同的是調(diào)用 Generator 函數(shù)后,該函數(shù)并不執(zhí)行,返回的也不是函數(shù)運行結(jié)果,而是一個指向內(nèi)部狀態(tài)的指針對象
當調(diào)用 quips() 生成器函數(shù)時發(fā)生什么?> 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 }
每當生成器執(zhí)行 yield 語句時,生成器的堆棧結(jié)構(gòu)(本地變量、參數(shù)、臨時值、生成器內(nèi)部當前的執(zhí)行位置 etc.)被移出堆棧,然而生成器對象保留對這個堆棧結(jié)構(gòu)的引用(備份),所以稍后調(diào)用 .next() 可以重新激活堆棧結(jié)構(gòu)并且繼續(xù)執(zhí)行。當生成器運行時,它和調(diào)用者處于同一線程中,擁有確定的連續(xù)執(zhí)行順序,永不并發(fā)。
遍歷器對象的 next 方法的運行邏輯如下:
遇到 yield 表達式,就暫停執(zhí)行后面的操作,并將緊跟在 yield 后面的那個表達式的值,作為返回的對象的 value 屬性值
下一次調(diào)用 next 方法時,再繼續(xù)往下執(zhí)行,直到遇到下一個 yield 表達式
如果沒有再遇到新的 yield 表達式,就一直運行到函數(shù)結(jié)束,直到 return 語句為止,并將 return 語句后面的表達式的值,作為返回的對象的 value 屬性值
如果該函數(shù)沒有 return 語句,則返回的對象的 value 屬性值為 undefined
生成器是迭代器!迭代器是 ES6 中獨立的內(nèi)建類,同時也是語言的一個擴展點,通過實現(xiàn) [Symbol.iterator]() 和 .next() 兩個方法就可以創(chuàng)建自定義迭代器。
// 應該彈出三次 "ding" for (var value of range(0, 3)) { alert("Ding! at floor #" + value); }
我們可以使用生成器實現(xiàn)上面循環(huán)中的 range 方法:
function* range(start, stop) { for (var i = start; i < stop; i++) yield i; }
生成器是迭代器,所有的生成器都有內(nèi)建 .next() 和 [Symbol.iterator]() 方法的實現(xiàn),我們只需要編寫循環(huán)部分的行為即可。
for...of 循環(huán)可以自動遍歷 Generator 函數(shù)時生成的 Iterator 對象,且此時不再需要調(diào)用 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 循環(huán),依次顯示 5 個 yield 表達式的值。這里需要注意,一旦 next 方法的返回對象的 done 屬性為 true,for...of 循環(huán)就會中止,且不包含該返回對象,所以上面代碼的 return 語句返回的6,不包括在 for...of 循環(huán)之中。
下面是一個利用 Generator 函數(shù)和 for...of 循環(huán),實現(xiàn)斐波那契數(shù)列的例子:
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 循環(huán)以外,擴展運算符(...)、解構(gòu)賦值和 Array.from 方法內(nèi)部調(diào)用的,都是遍歷器接口,這意味著它們都可以將 Generator 函數(shù)返回的 Iterator 對象作為參數(shù)。使用 Generator 實現(xiàn)生產(chǎn)消費者模式
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 誕生以前,異步編程的方法大概有下面四種:
回調(diào)函數(shù)
事件監(jiān)聽
發(fā)布/訂閱
Promise 對象
想必大家都經(jīng)歷過同樣的問題,在異步流程控制中會使用大量的回調(diào)函數(shù),甚至出現(xiàn)多個回調(diào)函數(shù)嵌套導致的情況,代碼不是縱向發(fā)展而是橫向發(fā)展,很快就會亂成一團無法管理,因為多個異步操作形成強耦合,只要有一個操作需要修改,它的上層回調(diào)函數(shù)和下層回調(diào)函數(shù),可能都要跟著修改,這種情況就是我們常說的"回調(diào)函數(shù)地獄"。
Promise 對象就是為了解決這個問題而提出的,它不是新的語法功能,而是一種新的寫法,允許將回調(diào)函數(shù)的嵌套,改成鏈式調(diào)用。然而,Promise 的最大問題就是代碼冗余,原來的任務被 Promise 包裝一下,不管什么操作一眼看去都是一堆 then,使得原來的語義變得很不清楚。
哈哈這里有些明知故問,答案當然就是 Generator!Generator 函數(shù)是協(xié)程在 ES6 的實現(xiàn),整個 Generator 函數(shù)就是一個封裝的異步任務,或者說是異步任務的容器,Generator 函數(shù)可以暫停執(zhí)行和恢復執(zhí)行,這是它能封裝異步任務的根本原因,除此之外,它還有兩個特性使它可以作為異步編程的完整解決方案:函數(shù)體內(nèi)外的數(shù)據(jù)交換和錯誤處理機制。
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 方法帶有參數(shù)2,這個參數(shù)可以傳入 Generator 函數(shù),作為上個階段異步任務的返回結(jié)果,被函數(shù)體內(nèi)的變量 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 函數(shù)體外,使用指針對象的 throw 方法拋出的錯誤,可以被函數(shù)體內(nèi)的 try...catch 代碼塊捕獲,這意味著出錯的代碼與處理錯誤的代碼實現(xiàn)了時間和空間上的分離,這對于異步編程無疑是很重要的。
Generator 函數(shù)的自動流程管理 Thunk 函數(shù)函數(shù)的"傳值調(diào)用"和“傳名調(diào)用”一直以來都各有優(yōu)劣(比如傳值調(diào)用比較簡單,但是對參數(shù)求值的時候,實際上還沒用到這個參數(shù),有可能造成性能損失),本文不多贅述,在這里需要提到的是:編譯器的“傳名調(diào)用”實現(xiàn),往往是將參數(shù)放到一個臨時函數(shù)之中,再將這個臨時函數(shù)傳入函數(shù)體,這個臨時函數(shù)就叫做 Thunk 函數(shù)。
function f(m) { return m * 2; } f(x + 5); // 等同于 var thunk = function () { return x + 5; }; function f(thunk) { return thunk() * 2; }
任何函數(shù),只要參數(shù)有回調(diào)函數(shù),就能寫成 Thunk 函數(shù)的形式。下面是一個簡單的 JavaScript 語言 Thunk 函數(shù)轉(zhuǎn)換器:
// 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 函數(shù)有什么用?回答是以前確實沒什么用,但是 ES6 有了 Generator 函數(shù),Thunk 函數(shù)現(xiàn)在可以用于 Generator 函數(shù)的自動流程管理。
首先 Generator 函數(shù)本身是可以自動執(zhí)行的:
function* gen() { // ... } var g = gen(); var res = g.next(); while(!res.done){ console.log(res.value); res = g.next(); }
但是,這并不適合異步操作,如果必須保證前一步執(zhí)行完,才能執(zhí)行后一步,上面的自動執(zhí)行就不可行,這時 Thunk 函數(shù)就能派上用處,以讀取文件為例,下面的 Generator 函數(shù)封裝了兩個異步操作:
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 命令用于將程序的執(zhí)行權移出 Generator 函數(shù),那么就需要一種方法,將執(zhí)行權再交還給 Generator 函數(shù),這種方法就是 Thunk 函數(shù),因為它可以在回調(diào)函數(shù)里,將執(zhí)行權交還給 Generator 函數(shù),為了便于理解,我們先看如何手動執(zhí)行上面這個 Generator 函數(shù):
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); }); });
仔細查看上面的代碼,可以發(fā)現(xiàn) Generator 函數(shù)的執(zhí)行過程,其實是將同一個回調(diào)函數(shù),反復傳入 next 方法的 value 屬性,這使得我們可以用遞歸來自動完成這個過程,下面就是一個基于 Thunk 函數(shù)的 Generator 執(zhí)行器:
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 函數(shù)并不是 Generator 函數(shù)自動執(zhí)行的唯一方案,因為自動執(zhí)行的關鍵是,必須有一種機制自動控制 Generator 函數(shù)的流程,接收和交還程序的執(zhí)行權,回調(diào)函數(shù)可以做到這一點,Promise 對象也可以做到這一點。
文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/94428.html
摘要:本文先回顧生成器,然后過渡到協(xié)程編程。其作用主要體現(xiàn)在三個方面數(shù)據(jù)生成生產(chǎn)者,通過返回數(shù)據(jù)數(shù)據(jù)消費消費者,消費傳來的數(shù)據(jù)實現(xiàn)協(xié)程。解決回調(diào)地獄的方式主要有兩種和協(xié)程。重點應當關注控制權轉(zhuǎn)讓的時機,以及協(xié)程的運作方式。 轉(zhuǎn)載請注明文章出處: https://tlanyan.me/php-review... PHP回顧系列目錄 PHP基礎 web請求 cookie web響應 sess...
摘要:線程擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程亦由操作系統(tǒng)調(diào)度標準線程是的。以及鳥哥翻譯的這篇詳細文檔我就以他實現(xiàn)的協(xié)程多任務調(diào)度為基礎做一下例子說明并說一下關于我在阻塞方面所做的一些思考。 進程、線程、協(xié)程 關于進程、線程、協(xié)程,有非常詳細和豐富的博客或者學習資源,我不在此做贅述,我大致在此介紹一下這幾個東西。 進程擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,進程由操作系...
摘要:傳統(tǒng)的異步方法回調(diào)函數(shù)事件監(jiān)聽發(fā)布訂閱之前寫過一篇關于的文章,里邊寫過關于異步的一些概念。內(nèi)部函數(shù)就是的回調(diào)函數(shù),函數(shù)首先把函數(shù)的指針指向函數(shù)的下一步方法,如果沒有,就把函數(shù)傳給函數(shù)屬性,否則直接退出。 Generator函數(shù)與異步編程 因為js是單線程語言,所以需要異步編程的存在,要不效率太低會卡死。 傳統(tǒng)的異步方法 回調(diào)函數(shù) 事件監(jiān)聽 發(fā)布/訂閱 Promise 之前寫過一篇關...
摘要:協(xié)程,又稱微線程,纖程。最大的優(yōu)勢就是協(xié)程極高的執(zhí)行效率。生產(chǎn)者產(chǎn)出第條數(shù)據(jù)返回更新值更新消費者正在調(diào)用第條數(shù)據(jù)查看當前進行的線程函數(shù)中有,返回值為生成器庫實現(xiàn)協(xié)程通過提供了對協(xié)程的基本支持,但是不完全。 協(xié)程,又稱微線程,纖程。英文名Coroutine協(xié)程看上去也是子程序,但執(zhí)行過程中,在子程序內(nèi)部可中斷,然后轉(zhuǎn)而執(zhí)行別的子程序,在適當?shù)臅r候再返回來接著執(zhí)行。 最大的優(yōu)勢就是協(xié)程極高...
閱讀 2091·2021-11-23 10:13
閱讀 2799·2021-11-09 09:47
閱讀 2743·2021-09-22 15:08
閱讀 3323·2021-09-03 10:46
閱讀 2239·2019-08-30 15:54
閱讀 921·2019-08-28 18:09
閱讀 2433·2019-08-26 18:26
閱讀 2346·2019-08-26 13:48