自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

JavaScript 異步編程指南 — 關(guān)于協(xié)程的一些思考

開發(fā) 前端
在了解協(xié)程之前,先看進(jìn)程、線程分別是什么,分享一個筆者之前寫的 Node.js 進(jìn)階之進(jìn)程與線程 文中結(jié)合 Node.js 列舉了一些示例,也是從一些基礎(chǔ)的層面來理解。

[[405491]]

本文轉(zhuǎn)載自微信公眾號「五月君」,作者五月君。轉(zhuǎn)載本文請聯(lián)系五月君公眾號。

從 Callback 到 Promise 的 .then().then()... 也是在不斷嘗試去解決異步編程帶來的回調(diào)嵌套、錯誤管理等問題,Promise 進(jìn)一步解決了這些問題,但是當(dāng)異步鏈多了之后你會發(fā)現(xiàn)代碼會變成這樣 .then().then()... 由原來的橫向變成了縱向的模式,仍就存在冗余的代碼,基于我們大腦對事物的思考,我們更傾向于一種近乎 “同步” 的寫法來表達(dá)我們的異步代碼,在 ES6 規(guī)范中為我們提供了 Generator 函數(shù)進(jìn)一步改善我們的代碼編寫方式。

Generator 中文翻譯過來我們可以稱呼它為 “生成器”,它擁有函數(shù)的執(zhí)行權(quán),知道什么時候暫停、什么時候執(zhí)行,這里還有一個概念協(xié)程,有些地方也看到過一些提問:“JavaScript 中有協(xié)程嗎?” “Node.js 中有協(xié)程嗎?” 這些問題正是本文討論的,本節(jié)著重從概念上讓大家做一些了解,認(rèn)識到協(xié)程在 JavaScript 是怎么樣的存在。

進(jìn)程 VS 線程 VS 協(xié)程?

在了解協(xié)程之前,先看進(jìn)程、線程分別是什么,分享一個筆者之前寫的 Node.js 進(jìn)階之進(jìn)程與線程 文中結(jié)合 Node.js 列舉了一些示例,也是從一些基礎(chǔ)的層面來理解。

進(jìn)程

進(jìn)程(Process)是計算機(jī)中的程序關(guān)于某數(shù)據(jù)集合上的一次運(yùn)行活動,是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位,是操作系統(tǒng)結(jié)構(gòu)的基礎(chǔ),進(jìn)程是線程的容器(來自百科)。

我們啟動一個服務(wù)、運(yùn)行一個實(shí)例,就是開一個服務(wù)進(jìn)程,例如 Java 里的 JVM 本身就是一個進(jìn)程,Node.js 里通過 node app.js 開啟一個服務(wù)進(jìn)程,多進(jìn)程就是進(jìn)程的復(fù)制(fork),fork 出來的每個進(jìn)程都擁有自己的獨(dú)立空間地址、數(shù)據(jù)棧,一個進(jìn)程無法訪問另外一個進(jìn)程里定義的變量、數(shù)據(jù)結(jié)構(gòu),只有建立了 IPC 通信,進(jìn)程之間才可數(shù)據(jù)共享。

Mac 系統(tǒng)自帶的監(jiān)控工具 “活動監(jiān)視器” 也可看到效果。

Node.js 中我們通過 Cluster 模塊創(chuàng)建多進(jìn)程時為什么要根據(jù) CPU 核心數(shù)?創(chuàng)建更多不好嗎?在一個 CPU 核心的任何時間內(nèi)只能執(zhí)行一個進(jìn)程。因此,當(dāng)你 CPU 核心數(shù)有限時,創(chuàng)建過多的進(jìn)程,CPU 也是忙不過來的。

Node.js 通過單線程 + 事件循環(huán)解決了并發(fā)問題。而我們使用 Node.js 利用 Cluster 模塊根據(jù) CPU 核心數(shù)創(chuàng)建多進(jìn)程解決的是并行問題,假設(shè)我有 4 CPU 每個 CPU 分別對應(yīng)一個線程并行處理 A、B、C、D 不同的任務(wù),線程之間互不搶占資源。

一句話總結(jié):進(jìn)程之間數(shù)據(jù)完全隔離、由操作系統(tǒng)調(diào)度,自動切換上下文信息,屬系統(tǒng)層級的構(gòu)造。

線程

線程是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位,首先我們要清楚線程是隸屬于進(jìn)程的,被包含于進(jìn)程之中。一個線程只能隸屬于一個進(jìn)程,但是一個進(jìn)程是可以擁有多個線程的。

同一塊代碼,可以根據(jù)系統(tǒng) CPU 核心數(shù)啟動多個進(jìn)程,每個進(jìn)程都有屬于自己的獨(dú)立運(yùn)行空間,進(jìn)程之間是不相互影響的。同一進(jìn)程中的多條線程將共享該進(jìn)程中的全部系統(tǒng)資源,如虛擬地址空間,文件描述符和信號處理等。但同一進(jìn)程中的多個線程有各自的調(diào)用棧(call stack),自己的寄存器環(huán)境(register context),自己的線程本地存儲(thread-local storage),線程又有單線程和多線程之分,具有代表性的 JavaScript、Java 語言。

線程共享進(jìn)程的資源,可以由系統(tǒng)調(diào)度運(yùn)行,可以自動完成線程切換,也許你會聽到多線程編程、并發(fā)問題,首先,并發(fā)指的某個時間點(diǎn)多個任務(wù)隊列對應(yīng)到同一個 CPU 上運(yùn)行,在任一時間點(diǎn)內(nèi)也只會有一個任務(wù)隊列在 CPU 上執(zhí)行,這時就產(chǎn)生排隊了。

為了解決這個問題,CPU 運(yùn)行時間片會被分成多個 CPU 時間段,每個時間段給各個任務(wù)隊列執(zhí)行(對應(yīng)多個線程),這樣解決了一個任務(wù)如果造成阻塞,不會影響到其它的任務(wù)運(yùn)行,同樣線程是會自動切換的。

Node.js 是怎么解決的并發(fā)問題?Node.js 主線程是單線程的,核心通過事件循環(huán),每次循環(huán)時取出任務(wù)隊列中的可執(zhí)行任務(wù)運(yùn)行,沒有多線程上下文切換,資源搶占問題,達(dá)到高并發(fā)成就。

一句話總結(jié):線程之間大多數(shù)共享數(shù)據(jù)(各自的調(diào)用棧這些信息除外),由操作系統(tǒng)調(diào)用,自動切換上下文,系統(tǒng)層級的構(gòu)造。

協(xié)程

協(xié)程又稱為微線程、纖程,英文 Coroutine。協(xié)程類似于線程,但是協(xié)程是協(xié)作式多任務(wù)的,而線程是搶占式多任務(wù)的。協(xié)程之間的調(diào)用不需要涉及任何系統(tǒng)調(diào)用,是語言層級的構(gòu)造,可看作一種形式的控制流,有時候我們也會稱它為用戶態(tài)的輕量級線程。

協(xié)程一個特點(diǎn)是通過關(guān)鍵字 yield 調(diào)用其它協(xié)程,接下來每次協(xié)程被調(diào)用時,從協(xié)程上次 yield 返回的位置接著執(zhí)行,這種通過 yield 協(xié)作轉(zhuǎn)移執(zhí)行權(quán)的操作,彼此沒有調(diào)用者和被調(diào)用者的關(guān)系,是彼此平等對稱的一種關(guān)系。

協(xié)程與線程兩者的差異,可以看出 “同一時間如果有多個線程,但它們會都處于運(yùn)行狀態(tài),線程是搶占式的,而協(xié)程同一時間運(yùn)行的只有一個,其它的協(xié)程處于暫停狀態(tài),執(zhí)行權(quán)由協(xié)程自己分配”。

協(xié)程也不是萬能的,它需要配合異步 I/O 才能發(fā)揮最好的效果,對于操作系統(tǒng)而言是不知道協(xié)程的存在的,它只知道線程。需要注意,如果一個協(xié)程遇到了阻塞的 I/O 調(diào)用,這時會導(dǎo)致操作系統(tǒng)讓線程阻塞,那么在這個線程上的其它協(xié)程也都會陷入阻塞。

一句話總結(jié):協(xié)程共享數(shù)據(jù),由程序控制完成上下文切換,語言層級的構(gòu)造。

JavaScript 有協(xié)程嗎

之前知乎上有個問題 “Node.js 真的有協(xié)程嗎?” 協(xié)程在很多語言中都支持,只是每個實(shí)現(xiàn)略有差異,下圖來自維基百科展示了支持協(xié)程的編程語言,可以看到 JavaScript 在 ECMAScript 6 支持,ECMAScript 7 之后通過 await 支持,Node.js 做為 JavaScript 在服務(wù)端的運(yùn)行時,只要你的 Node.js 版本對應(yīng)支持,就是可以的。

協(xié)程在 JavaScript 中的實(shí)現(xiàn)

生成器與協(xié)程

生成器(Generator)是協(xié)程的子集,也稱為 “半?yún)f(xié)程”。差異在于,生成器只能把控制權(quán)交給它的調(diào)用者,完全協(xié)程有能力控制在它讓位之后哪個協(xié)程立即接續(xù)它執(zhí)行。在 JavaScript 里我們說的 Generator 函數(shù)就是 ES6 對協(xié)程的實(shí)現(xiàn)。

JavaScript 是一個單線程的語言,只能保持一個調(diào)用棧。在異步操作的回調(diào)函數(shù)里,一旦出錯原始的調(diào)用棧早已結(jié)束,引入?yún)f(xié)程之后每個任務(wù)可以保持自己的調(diào)用棧,這樣解決的一大問題是出錯誤時可以找到原始的調(diào)用棧。

看下生成器函數(shù)與普通函數(shù)有什么區(qū)別?首先普通函數(shù)通過棧實(shí)現(xiàn)的,舉個例子,調(diào)用時是 A() -> B() -> C() 入棧,最后是 C() -> B() -> A() 這樣一個順序最后進(jìn)入的先出棧執(zhí)行。

生成器函數(shù)看似和普通函數(shù)相似,其實(shí)內(nèi)部執(zhí)行機(jī)制是完全不同的,生成器函數(shù)在內(nèi)部執(zhí)行遇到 yield 會交出函數(shù)的執(zhí)行權(quán)給其它協(xié)程(此處類似 CPU 中斷),轉(zhuǎn)而去執(zhí)行別的任務(wù),在將來一段時間后等到執(zhí)行權(quán)返回(生成器還會把控制權(quán)交給它的調(diào)用者),程序再從暫停的地方繼續(xù)執(zhí)行。

無堆棧協(xié)程

自 ES6 開始,通過 “Generator” 和 “yield” 表達(dá)式提供了無堆棧協(xié)程功能。

“無棧協(xié)程的秘密在于它們只能從頂級函數(shù)中掛起自己。對于其他所有函數(shù),它們的數(shù)據(jù)都分配在被調(diào)用者堆棧上,因此從協(xié)程調(diào)用的所有函數(shù)必須在掛起協(xié)程之前完成。協(xié)程保留其狀態(tài)所需的所有數(shù)據(jù)都在堆上動態(tài)分配。這通常需要幾個局部變量和參數(shù),其大小遠(yuǎn)小于預(yù)先分配的整個堆棧”。參考 coroutines-introduction

棧是一塊連續(xù)的內(nèi)存,能夠從子函數(shù)產(chǎn)生的協(xié)程稱為棧式,它們可以記住整個調(diào)用棧,這種也稱為棧式協(xié)程。在 JavaScript 中我們只能從生成器函數(shù)內(nèi)部暫停、恢復(fù)執(zhí)行生成器函數(shù)。

下面示例 test1() 是生成器函數(shù),但是 forEach 里面的匿名函數(shù)是一個普通的函數(shù),就無法在內(nèi)部使用 yield 關(guān)鍵字,運(yùn)行時會拋出錯誤 “SyntaxError: Unexpected identifier”

  1. function *test1() { 
  2.   console.log('execution start'); 
  3.    
  4.   ['A''B'].forEach(function(item) { 
  5.     yield item; 
  6.   }) 

生成器函數(shù)示例

例如,現(xiàn)在有兩個生成器函數(shù) test1()、test2(),還有 co 這個工具可以幫助我們自動的執(zhí)行生成器函數(shù)。

  1. const co = require('co'); 
  2. function *test1() { 
  3.   console.log('execution 1'); 
  4.   console.log(yield Promise.resolve(1)); 
  5.   console.log('execution 2'); 
  6.   console.log(yield Promise.resolve(2)); 
  7.  
  8. function *test2() { 
  9.   console.log('execution a'); 
  10.   console.log(yield Promise.resolve('a')); 
  11.   console.log('execution b'); 
  12.   console.log(yield Promise.resolve('b')); 
  13.  
  14. co(test1); 
  15. co(test2); 

看下運(yùn)行結(jié)果:

  • 第一次程序執(zhí)行 test1() 函數(shù),先輸出 'execution 1' 遇到 yield 語句程序的控制權(quán)轉(zhuǎn)移。
  • 現(xiàn)在執(zhí)行權(quán)轉(zhuǎn)移到了 test2() 函數(shù),執(zhí)行代碼輸出 'execution a' 當(dāng)遇到 yield 語句后交出程序的控制權(quán)。
  • 此時 test1() 函數(shù)收回執(zhí)行權(quán),恢復(fù)執(zhí)行輸出 '1' 繼續(xù)往下執(zhí)行輸出 'execution 2' 當(dāng)遇到 yield 語句再次交出執(zhí)行權(quán),依次類推。
  1. execution 1 
  2. execution a 
  3. execution 2 
  4. execution b 

總結(jié)

“JavaScript 有協(xié)程嗎?” JavaScript 中是在 ES6 后基于生成器函數(shù)(Generator)實(shí)現(xiàn)的,生成器只能把程序的執(zhí)行權(quán)還給它的調(diào)用者,這種方式我們稱為 “半?yún)f(xié)程”,而完全的協(xié)程是任何函數(shù)都可讓暫停的協(xié)程執(zhí)行。

基于生成器函數(shù)這種寫法,如果去掉 yield 關(guān)鍵字,與我們普通的函數(shù)是相似的,以一種同步的方式來表達(dá),解決了回調(diào)嵌套的問題,另外我們還可以通過 try...catch 做錯誤捕獲,只不過我們還需要借助 CO 這樣的模塊,讓生成器函數(shù)自動執(zhí)行,這個問題在 ES7 中已經(jīng)得到了更好地解決,我們可以通過 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

 

責(zé)任編輯:武曉燕 來源: 五月君
相關(guān)推薦

2015-10-12 08:59:57

異步代碼測試

2017-08-10 15:50:44

PHP協(xié)程阻塞

2017-12-21 07:54:07

2021-06-10 10:02:19

優(yōu)化緩存性能

2020-08-20 10:16:56

Golang錯誤處理數(shù)據(jù)

2024-12-27 10:51:53

2012-12-19 09:36:49

測試自動化測試

2021-09-16 09:59:13

PythonJavaScript代碼

2009-08-27 11:02:22

JavaScript事

2023-11-29 07:10:50

python協(xié)程異步編程

2020-02-03 16:03:36

疫情思考

2009-06-25 09:50:32

JSF

2021-06-10 20:17:04

云網(wǎng)融合超融合

2021-08-08 10:44:33

安卓系統(tǒng)開發(fā)者手機(jī)廠商

2018-06-29 14:51:41

Java健壯性實(shí)踐

2011-11-30 15:57:18

2015-03-30 11:21:27

編程編程反思

2011-01-19 10:50:31

軟件設(shè)計師

2011-07-13 09:13:56

Android設(shè)計
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號