掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
本文轉(zhuǎn)載自微信公眾號(hào)「五月君」,作者五月君。轉(zhuǎn)載本文請(qǐng)聯(lián)系五月君公眾號(hào)。

從 Callback 到 Promise 的 .then().then()... 也是在不斷嘗試去解決異步編程帶來(lái)的回調(diào)嵌套、錯(cuò)誤管理等問(wèn)題,Promise 進(jìn)一步解決了這些問(wèn)題,但是當(dāng)異步鏈多了之后你會(huì)發(fā)現(xiàn)代碼會(huì)變成這樣 .then().then()... 由原來(lái)的橫向變成了縱向的模式,仍就存在冗余的代碼,基于我們大腦對(duì)事物的思考,我們更傾向于一種近乎 “同步” 的寫(xiě)法來(lái)表達(dá)我們的異步代碼,在 ES6 規(guī)范中為我們提供了 Generator 函數(shù)進(jìn)一步改善我們的代碼編寫(xiě)方式。
Generator 中文翻譯過(guò)來(lái)我們可以稱(chēng)呼它為 “生成器”,它擁有函數(shù)的執(zhí)行權(quán),知道什么時(shí)候暫停、什么時(shí)候執(zhí)行,這里還有一個(gè)概念協(xié)程,有些地方也看到過(guò)一些提問(wèn):“JavaScript 中有協(xié)程嗎?” “Node.js 中有協(xié)程嗎?” 這些問(wèn)題正是本文討論的,本節(jié)著重從概念上讓大家做一些了解,認(rèn)識(shí)到協(xié)程在 JavaScript 是怎么樣的存在。
在了解協(xié)程之前,先看進(jìn)程、線程分別是什么,分享一個(gè)筆者之前寫(xiě)的 Node.js 進(jìn)階之進(jìn)程與線程 文中結(jié)合 Node.js 列舉了一些示例,也是從一些基礎(chǔ)的層面來(lái)理解。
進(jìn)程(Process)是計(jì)算機(jī)中的程序關(guān)于某數(shù)據(jù)集合上的一次運(yùn)行活動(dòng),是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位,是操作系統(tǒng)結(jié)構(gòu)的基礎(chǔ),進(jìn)程是線程的容器(來(lái)自百科)。
我們啟動(dòng)一個(gè)服務(wù)、運(yùn)行一個(gè)實(shí)例,就是開(kāi)一個(gè)服務(wù)進(jìn)程,例如 Java 里的 JVM 本身就是一個(gè)進(jìn)程,Node.js 里通過(guò) node app.js 開(kāi)啟一個(gè)服務(wù)進(jìn)程,多進(jìn)程就是進(jìn)程的復(fù)制(fork),fork 出來(lái)的每個(gè)進(jìn)程都擁有自己的獨(dú)立空間地址、數(shù)據(jù)棧,一個(gè)進(jìn)程無(wú)法訪問(wèn)另外一個(gè)進(jìn)程里定義的變量、數(shù)據(jù)結(jié)構(gòu),只有建立了 IPC 通信,進(jìn)程之間才可數(shù)據(jù)共享。
Mac 系統(tǒng)自帶的監(jiān)控工具 “活動(dòng)監(jiān)視器” 也可看到效果。
Node.js 中我們通過(guò) Cluster 模塊創(chuàng)建多進(jìn)程時(shí)為什么要根據(jù) CPU 核心數(shù)?創(chuàng)建更多不好嗎?在一個(gè) CPU 核心的任何時(shí)間內(nèi)只能執(zhí)行一個(gè)進(jìn)程。因此,當(dāng)你 CPU 核心數(shù)有限時(shí),創(chuàng)建過(guò)多的進(jìn)程,CPU 也是忙不過(guò)來(lái)的。
Node.js 通過(guò)單線程 + 事件循環(huán)解決了并發(fā)問(wèn)題。而我們使用 Node.js 利用 Cluster 模塊根據(jù) CPU 核心數(shù)創(chuàng)建多進(jìn)程解決的是并行問(wèn)題,假設(shè)我有 4 CPU 每個(gè) CPU 分別對(duì)應(yīng)一個(gè)線程并行處理 A、B、C、D 不同的任務(wù),線程之間互不搶占資源。
一句話總結(jié):進(jìn)程之間數(shù)據(jù)完全隔離、由操作系統(tǒng)調(diào)度,自動(dòng)切換上下文信息,屬系統(tǒng)層級(jí)的構(gòu)造。
線程是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位,首先我們要清楚線程是隸屬于進(jìn)程的,被包含于進(jìn)程之中。一個(gè)線程只能隸屬于一個(gè)進(jìn)程,但是一個(gè)進(jìn)程是可以擁有多個(gè)線程的。
同一塊代碼,可以根據(jù)系統(tǒng) CPU 核心數(shù)啟動(dòng)多個(gè)進(jìn)程,每個(gè)進(jìn)程都有屬于自己的獨(dú)立運(yùn)行空間,進(jìn)程之間是不相互影響的。同一進(jìn)程中的多條線程將共享該進(jìn)程中的全部系統(tǒng)資源,如虛擬地址空間,文件描述符和信號(hào)處理等。但同一進(jìn)程中的多個(gè)線程有各自的調(diào)用棧(call stack),自己的寄存器環(huán)境(register context),自己的線程本地存儲(chǔ)(thread-local storage),線程又有單線程和多線程之分,具有代表性的 JavaScript、Java 語(yǔ)言。
線程共享進(jìn)程的資源,可以由系統(tǒng)調(diào)度運(yùn)行,可以自動(dòng)完成線程切換,也許你會(huì)聽(tīng)到多線程編程、并發(fā)問(wèn)題,首先,并發(fā)指的某個(gè)時(shí)間點(diǎn)多個(gè)任務(wù)隊(duì)列對(duì)應(yīng)到同一個(gè) CPU 上運(yùn)行,在任一時(shí)間點(diǎn)內(nèi)也只會(huì)有一個(gè)任務(wù)隊(duì)列在 CPU 上執(zhí)行,這時(shí)就產(chǎn)生排隊(duì)了。
為了解決這個(gè)問(wèn)題,CPU 運(yùn)行時(shí)間片會(huì)被分成多個(gè) CPU 時(shí)間段,每個(gè)時(shí)間段給各個(gè)任務(wù)隊(duì)列執(zhí)行(對(duì)應(yīng)多個(gè)線程),這樣解決了一個(gè)任務(wù)如果造成阻塞,不會(huì)影響到其它的任務(wù)運(yùn)行,同樣線程是會(huì)自動(dòng)切換的。
Node.js 是怎么解決的并發(fā)問(wèn)題?Node.js 主線程是單線程的,核心通過(guò)事件循環(huán),每次循環(huán)時(shí)取出任務(wù)隊(duì)列中的可執(zhí)行任務(wù)運(yùn)行,沒(méi)有多線程上下文切換,資源搶占問(wèn)題,達(dá)到高并發(fā)成就。
一句話總結(jié):線程之間大多數(shù)共享數(shù)據(jù)(各自的調(diào)用棧這些信息除外),由操作系統(tǒng)調(diào)用,自動(dòng)切換上下文,系統(tǒng)層級(jí)的構(gòu)造。
協(xié)程又稱(chēng)為微線程、纖程,英文 Coroutine。協(xié)程類(lèi)似于線程,但是協(xié)程是協(xié)作式多任務(wù)的,而線程是搶占式多任務(wù)的。協(xié)程之間的調(diào)用不需要涉及任何系統(tǒng)調(diào)用,是語(yǔ)言層級(jí)的構(gòu)造,可看作一種形式的控制流,有時(shí)候我們也會(huì)稱(chēng)它為用戶態(tài)的輕量級(jí)線程。
協(xié)程一個(gè)特點(diǎn)是通過(guò)關(guān)鍵字 yield 調(diào)用其它協(xié)程,接下來(lái)每次協(xié)程被調(diào)用時(shí),從協(xié)程上次 yield 返回的位置接著執(zhí)行,這種通過(guò) yield 協(xié)作轉(zhuǎn)移執(zhí)行權(quán)的操作,彼此沒(méi)有調(diào)用者和被調(diào)用者的關(guān)系,是彼此平等對(duì)稱(chēng)的一種關(guān)系。
協(xié)程與線程兩者的差異,可以看出 “同一時(shí)間如果有多個(gè)線程,但它們會(huì)都處于運(yùn)行狀態(tài),線程是搶占式的,而協(xié)程同一時(shí)間運(yùn)行的只有一個(gè),其它的協(xié)程處于暫停狀態(tài),執(zhí)行權(quán)由協(xié)程自己分配”。
協(xié)程也不是萬(wàn)能的,它需要配合異步 I/O 才能發(fā)揮最好的效果,對(duì)于操作系統(tǒng)而言是不知道協(xié)程的存在的,它只知道線程。需要注意,如果一個(gè)協(xié)程遇到了阻塞的 I/O 調(diào)用,這時(shí)會(huì)導(dǎo)致操作系統(tǒng)讓線程阻塞,那么在這個(gè)線程上的其它協(xié)程也都會(huì)陷入阻塞。
一句話總結(jié):協(xié)程共享數(shù)據(jù),由程序控制完成上下文切換,語(yǔ)言層級(jí)的構(gòu)造。
之前知乎上有個(gè)問(wèn)題 “Node.js 真的有協(xié)程嗎?” 協(xié)程在很多語(yǔ)言中都支持,只是每個(gè)實(shí)現(xiàn)略有差異,下圖來(lái)自維基百科展示了支持協(xié)程的編程語(yǔ)言,可以看到 JavaScript 在 ECMAScript 6 支持,ECMAScript 7 之后通過(guò) await 支持,Node.js 做為 JavaScript 在服務(wù)端的運(yùn)行時(shí),只要你的 Node.js 版本對(duì)應(yīng)支持,就是可以的。
生成器(Generator)是協(xié)程的子集,也稱(chēng)為 “半?yún)f(xié)程”。差異在于,生成器只能把控制權(quán)交給它的調(diào)用者,完全協(xié)程有能力控制在它讓位之后哪個(gè)協(xié)程立即接續(xù)它執(zhí)行。在 JavaScript 里我們說(shuō)的 Generator 函數(shù)就是 ES6 對(duì)協(xié)程的實(shí)現(xiàn)。
JavaScript 是一個(gè)單線程的語(yǔ)言,只能保持一個(gè)調(diào)用棧。在異步操作的回調(diào)函數(shù)里,一旦出錯(cuò)原始的調(diào)用棧早已結(jié)束,引入?yún)f(xié)程之后每個(gè)任務(wù)可以保持自己的調(diào)用棧,這樣解決的一大問(wèn)題是出錯(cuò)誤時(shí)可以找到原始的調(diào)用棧。
看下生成器函數(shù)與普通函數(shù)有什么區(qū)別?首先普通函數(shù)通過(guò)棧實(shí)現(xiàn)的,舉個(gè)例子,調(diào)用時(shí)是 A() -> B() -> C() 入棧,最后是 C() -> B() -> A() 這樣一個(gè)順序最后進(jìn)入的先出棧執(zhí)行。
生成器函數(shù)看似和普通函數(shù)相似,其實(shí)內(nèi)部執(zhí)行機(jī)制是完全不同的,生成器函數(shù)在內(nèi)部執(zhí)行遇到 yield 會(huì)交出函數(shù)的執(zhí)行權(quán)給其它協(xié)程(此處類(lèi)似 CPU 中斷),轉(zhuǎn)而去執(zhí)行別的任務(wù),在將來(lái)一段時(shí)間后等到執(zhí)行權(quán)返回(生成器還會(huì)把控制權(quán)交給它的調(diào)用者),程序再?gòu)臅和5牡胤嚼^續(xù)執(zhí)行。
自 ES6 開(kāi)始,通過(guò) “Generator” 和 “yield” 表達(dá)式提供了無(wú)堆棧協(xié)程功能。
“無(wú)棧協(xié)程的秘密在于它們只能從頂級(jí)函數(shù)中掛起自己。對(duì)于其他所有函數(shù),它們的數(shù)據(jù)都分配在被調(diào)用者堆棧上,因此從協(xié)程調(diào)用的所有函數(shù)必須在掛起協(xié)程之前完成。協(xié)程保留其狀態(tài)所需的所有數(shù)據(jù)都在堆上動(dòng)態(tài)分配。這通常需要幾個(gè)局部變量和參數(shù),其大小遠(yuǎn)小于預(yù)先分配的整個(gè)堆?!薄⒖?coroutines-introduction
棧是一塊連續(xù)的內(nèi)存,能夠從子函數(shù)產(chǎn)生的協(xié)程稱(chēng)為棧式,它們可以記住整個(gè)調(diào)用棧,這種也稱(chēng)為棧式協(xié)程。在 JavaScript 中我們只能從生成器函數(shù)內(nèi)部暫停、恢復(fù)執(zhí)行生成器函數(shù)。
下面示例 test1() 是生成器函數(shù),但是 forEach 里面的匿名函數(shù)是一個(gè)普通的函數(shù),就無(wú)法在內(nèi)部使用 yield 關(guān)鍵字,運(yùn)行時(shí)會(huì)拋出錯(cuò)誤 “SyntaxError: Unexpected identifier”
- function *test1() {
- console.log('execution start');
- ['A', 'B'].forEach(function(item) {
- yield item;
- })
- }
例如,現(xiàn)在有兩個(gè)生成器函數(shù) test1()、test2(),還有 co 這個(gè)工具可以幫助我們自動(dòng)的執(zhí)行生成器函數(shù)。
- const co = require('co');
- function *test1() {
- console.log('execution 1');
- console.log(yield Promise.resolve(1));
- console.log('execution 2');
- console.log(yield Promise.resolve(2));
- }
- function *test2() {
- console.log('execution a');
- console.log(yield Promise.resolve('a'));
- console.log('execution b');
- console.log(yield Promise.resolve('b'));
- }
- co(test1);
- co(test2);
看下運(yùn)行結(jié)果:
- execution 1
- execution a
- 1
- execution 2
- a
- execution b
- 2
- b
“JavaScript 有協(xié)程嗎?” JavaScript 中是在 ES6 后基于生成器函數(shù)(Generator)實(shí)現(xiàn)的,生成器只能把程序的執(zhí)行權(quán)還給它的調(diào)用者,這種方式我們稱(chēng)為 “半?yún)f(xié)程”,而完全的協(xié)程是任何函數(shù)都可讓暫停的協(xié)程執(zhí)行。
基于生成器函數(shù)這種寫(xiě)法,如果去掉 yield 關(guān)鍵字,與我們普通的函數(shù)是相似的,以一種同步的方式來(lái)表達(dá),解決了回調(diào)嵌套的問(wèn)題,另外我們還可以通過(guò) try...catch 做錯(cuò)誤捕獲,只不過(guò)我們還需要借助 CO 這樣的模塊,讓生成器函數(shù)自動(dòng)執(zhí)行,這個(gè)問(wèn)題在 ES7 中已經(jīng)得到了更好地解決,我們可以通過(guò) async/await 輕松的實(shí)現(xiàn)。
Reference
https://en.wikipedia.org/wiki/Coroutine#Implementations_in_JavaScript
https://zhuanlan.zhihu.com/p/70256971
http://zhangchen915.com/index.php/archives/719/
https://es6.ruanyifeng.com/#docs/generator

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流