天天看點

Node.js 子程序:你應該知道的一切

<b>本文講的是Node.js 子程序:你應該知道的一切,</b>

<b></b>

Node.js 子程式:你應該知道的一切

截圖來自我的視訊教學課程 - Node.js 進階

Node.js 的單線程、非阻塞執行特性在單程序下工作的很好。但是,單 CPU 中的單程序最終不足以處理應用中增長的工作負荷。

不管你的伺服器性能多麼強勁,單個線程隻能支援有限的負荷。

Node.js 運作于單線程之上并不意味着我們不能利用多程序,當然,也能運作在多台機器上。

使用多程序是擴充 Node 應用的最佳之道。Node.js 天生适合在多節點上建構分布式應用。這是它被命名為 “Node” 的原因。可擴充性被深深烙印進平台,自應用誕生之初就已經存在。

請注意,在閱讀這篇文章之前,你需要對 Node.js 的事件和流有足夠的了解。如果還沒有,我推薦你先去讀下面兩篇文章:

Node.js 子程式:你應該知道的一切
Node.js 子程式:你應該知道的一切

我們可以使用 Node 的 <code>child_process</code> 子產品來簡單地創造子程序,子程序之間可以通過消息系統簡單的通信。

<code>child_process</code> 子產品通過在一個子程序中執行系統指令,賦予我們使用作業系統功能的能力。

注意:這篇文章舉的所有例子都基于 Linux。如果在 Windows 上,你要切換為它們對應的 Window 指令。

Node.js 裡建立子程序有四種不同的方式:<code>spawn()</code>, <code>fork()</code>, <code>exec()</code> 和 <code>execFile()</code>。

我們将學習這四個函數之間的差別及其使用場景。

<code>spawn</code> 函數會在一個新的程序中啟動一條指令,我們可以使用它來給這條指令傳遞任意參數。比如,下面的代碼會衍生一個執行 <code>pwd</code> 指令的新程序。

我們簡單地從 <code>child_process</code> 子產品中解構 <code>spawn</code> 函數,然後将系統指令作為第一個參數來執行該函數。

上面的處理器給出了子程序的退出 <code>code</code> 和 <code>signal</code>,這兩個變量可以用來終止子程序。子程序正常退出時 <code>signal</code> 變量為 null。

<code>ChildProcess</code> 執行個體上還可以注冊 <code>disconnect</code>、<code>error</code>、<code>close</code> 和 <code>message</code> 事件。

<code>disconnect</code> 事件在父程序手動調用 <code>child.disconnect</code> 函數時觸發。

如果程序不能被衍生或者殺死,會觸發 <code>error</code> 事件。

<code>close</code> 事件在子程序的 <code>stdio</code> 流關閉時觸發。

<code>message</code> 事件最為重要。它在子程序使用 <code>process.send()</code> 函數來傳遞消息時觸發。這就是父/子程序間通信的原理。下面将給出一個例子。

每一個子程序還有三個标準 <code>stdio</code> 流,我們可以分别使用 <code>child.stdin</code>、<code>child.stdout</code> 和 <code>child.stderr</code> 來使用這三個流。

當這幾個流被關閉後,使用了它們的子程序會觸發 <code>close</code> 事件。這裡的 <code>close</code> 事件不同于 <code>exit</code> 事件,因為多個子程序可能共享相同的 <code>stdio</code> 流,是以一個子程序退出并不意味着流已經被關閉了。

既然所有的流都是事件觸發器,我們可以在歸屬于每個子程序的 <code>stdio</code> 流上監聽不同的事件。不像普通的程序,在子程序中,<code>stdout</code>/<code>stderr</code> 流是可讀流,而 <code>stdin</code> 流是可寫的。這基本上和主程序相反。這些流支援的事件都是标準的。最重要的是,在可讀流上我們可以監聽 <code>data</code> 事件,通過 <code>data</code> 事件可以得到任一指令的輸出或者執行指令過程中發生的錯誤:

上述兩個處理器會輸出兩者的日志到主程序的 <code>stdout</code> 和 <code>stderr</code> 事件上。當我們執行前面的 <code>spawn</code> 函數時,<code>pwd</code> 指令的輸出會被列印出來,并且子程序帶着代碼 <code>0</code> 退出,這表示沒有錯誤發生。

我們可以給指令傳遞參數,指令由 <code>spawn</code> 函數執行,<code>spawn</code> 函數用上了第二個參數,這是一個傳遞給該指令的所有參數組成的數組。比如說,為了在目前目錄執行 <code>find</code> 指令,并帶上一個 <code>-type f</code> 參數(用于列出所有檔案),我們可以這樣做:

如果這條指令的執行過程中出現錯誤,舉個例子,如果我們在 find 一個非法的目标檔案,<code>child.stderr</code> <code>data</code> 事件處理器将會被觸發,<code>exit</code> 事件處理器會報出一個退出代碼 <code>1</code>,這标志着出現了錯誤。錯誤的值最終取決于宿主作業系統和錯誤類型。

子程序中的 <code>stdin</code> 是一個可寫流。我們可以用它給指令發送一些輸入。就跟所有的可寫流一樣,消費輸入最簡單的方式是使用 <code>pipe</code> 函數。我們可以簡單地将可讀流管道化到可寫流。既然主線程的 <code>stdin</code> 是一個可讀流,我們可以将其管道化到子程序的 <code>stdin</code> 流。舉個例子:

在這個例子中,子程序調用 <code>wc</code> 指令,該指令可以統計 Linux 中的行數、單詞數和字元數。我們然後将主程序的 <code>stdin</code> 管道化到子程序的 <code>stdin</code>(一個可寫流)。這個組合的結果是,我們得到了一個标準輸入模式,在這個模式下,我們可以輸入一些字元。當敲下 <code>Ctrl+D</code> 時,輸入的内容将會作為 <code>wc</code> 指令的輸入。

Node.js 子程式:你應該知道的一切

Gif 截圖來自我的視訊教學課程 - Node.js 進階

我們也可以将多個程序的标準輸入/輸出互相用管道連接配接,就像 Linux 指令那樣。比如說,我們可以管道化 <code>find</code> 指令的<code>stdout</code> 到 <code>wc</code> 指令的 <code>stdin</code>,這樣可以統計目前目錄的所有檔案。

我給 <code>wc</code> 指令添加了 <code>-l</code> 參數,使它隻統計行數。當執行完畢,上述代碼會輸出目前目錄下所有子目錄檔案的行數。

預設情況下,<code>spawn</code> 函數并不為我們傳進的指令而建立一個 <code>shell</code> 來執行,這使得它相比建立 shell 的 <code>exec</code> 函數,效率略微更高。<code>exec</code> 函數還有另一個主要的差別,它緩沖了指令生成的輸出,并傳遞整個輸出值給一個回調函數(而不是使用流,那是 <code>spawn</code> 的做法)。

這裡給出了之前 <code>find | wc</code> 例子的 <code>exec</code> 函數實作。

既然 <code>exec</code> 函數使用 shell 執行指令,我們可以使用 shell 文法 來直接利用 shell 管道特性。

當 <code>stdout</code> 參數存在,<code>exec</code> 函數緩沖輸出并傳遞它給回調函數(<code>exec</code> 的第二個參數)。這裡的 <code>stdout</code> 參數是指令的輸出,我們要将其列印出來。

如果你需要使用 shell 文法,并且來自指令的資料規模較小,<code>exec</code> 函數是個不錯的選擇。(記住,<code>exec</code> 會在傳回之前,緩沖所有資料進記憶體。)

當指令預期的資料規模比較大時,選擇 <code>spawn</code> 函數會好得多,因為資料将會和标準 IO 對象被流式處理。

我們可以令衍生的子程序繼承其父程序的标準 IO 對象,但更重要的是,我們同樣可以令 <code>spawn</code> 函數使用 shell 文法。下面同樣是 <code>find | wc</code> 指令, 由 <code>spawn</code> 函數實作:

因為有上面的 <code>stdio: 'inherit'</code> 選項,當代碼執行時,子程序繼承主程序的 <code>stdin</code>、<code>stdout</code> 和 <code>stderr</code>。這造成子程序的資料事件處理器在主程序的 <code>process.stdout</code> 流上被觸發,使得腳本立即輸出結果。

<code>shell: true</code> 選項使我們可以在傳遞的指令中使用 shell 文法,就像之前的 <code>exec</code> 例子中那樣。但這段代碼還可以利用 <code>spawn</code>函數帶來的資料的流式。真正實作了共赢。

除了 <code>shell</code> 和 <code>stdio</code>,<code>child_process</code> 函數的最後一個參數還有其他可以的選項。比如,使用 <code>cwd</code> 選項改變腳本的工作目錄。舉個例子,這裡有個和前述相同的統計所有檔案數量的例子,它利用 <code>spawn</code> 函數實作,使用了一個 shell 指令,并把工作目錄設定為我的 Downloads 檔案夾。這裡的 <code>cwd</code> 選項會讓腳本統計 <code>~/Downloads</code> 裡的所有檔案數量。

另一個可以使用的選項是 <code>env</code>,它可以指定哪些環境變量對于子程序是可見的。此選項的預設值是 <code>process.env</code>,這會賦予所有指令通路目前程序上下文環境的權限。如果想覆寫預設行為,我們可以簡單地傳遞一個空對象,或者是作為唯一的環境變量的新值給 <code>env</code> 選項:

上面的 echo 指令沒有通路父程序環境變量的權限。比如,它不能通路 <code>$HOME</code> 目錄,但它可以通路 <code>$ANSWER</code> 目錄,因為通過<code>env</code> 選項,它被傳遞了一個指定的環境變量。

這裡要解釋的最後一個重要的子程序選項,<code>detached</code> 選項,使子程序獨立于父程序運作。

假設有個檔案 <code>timer.js</code>,使事件循環一直忙碌運作:

我們可以使用 <code>detached</code> 選項,在背景執行這段代碼:

分離的子程序的具體行為取決于作業系統。Windows 上,分離的子程序有自己的控制台視窗,然而在 Linux 上,分離的子程序會成為新的程序組和會話的上司程序。

如果 <code>unref</code> 函數在分離的子程序中被調用,父程序可以獨立于子程序退出。如果子程序是一個長期運作的程序,這個函數會很有用。但為了保持子程序在背景運作,子程序的 <code>stdio</code> 配置也必須獨立于父程序。

上述例子會在背景運作一個 node 腳本(<code>timer.js</code>),通過分離和忽略其父程序的 <code>stdio</code> 檔案描述符來實作。是以當子程序在背景運作時,父程序可以随時終止。

Node.js 子程式:你應該知道的一切

Gif 來自我的視訊教學課程 - Node.js 進階

如果你不想用 shell 執行一個檔案,那麼 execFile 函數正是你想要的。它的行為跟 <code>exec</code> 函數一模一樣,但沒有使用 shell,這會讓它更有效率。Windows 上,一些檔案不能在它們自己之上執行,比如 <code>.bat</code> 或者 <code>.cmd</code> 檔案。這些檔案不能使用<code>execFile</code> 執行,并且執行它們時,需要将 shell 設定為 true,且隻能使用 <code>exec</code>、<code>spawn</code> 兩者之一。

所有 <code>child_process</code> 子產品都有同步阻塞版本,它們會一直等待直到子程序退出。

Node.js 子程式:你應該知道的一切

這些同步版本在簡化腳本任務或一些啟動程序任務上,一定程度上有所幫助。但除此之外,我們應該避免使用它們。

<code>fork</code> 函數是 <code>spawn</code> 函數針對衍生 node 程序的一個變種。<code>spawn</code> 和 <code>fork</code> 最大的差別在于,使用 <code>fork</code> 時,通信頻道建立于子程序,是以我們可以在 fork 出來的程序上使用 <code>send</code> 函數,這些程序上有個全局 <code>process</code> 對象,可以用于父程序和 fork 程序之間傳遞消息。這個函數通過 <code>EventEmitter</code> 子產品接口實作。這裡有個例子:

父檔案,<code>parent.js</code>:

子檔案,<code>child.js</code>:

上面的父檔案中,我們 fork <code>child.js</code>(将會通過 <code>node</code> 指令執行檔案),并監聽 <code>message</code> 事件。一旦子程序使用<code>process.send</code>,事實上我們每秒都在執行它,<code>message</code> 事件就會被觸發,

為了實作父程序向下給子程序傳遞消息,我們可以在 fork 的對象本身上執行 <code>send</code> 函數,然後在子檔案中,在全局 <code>process</code>對象上監聽 <code>message</code> 事件。

執行上面的 <code>parent.js</code> 檔案時,它将首先向下發送 <code>{ hello: 'world' }</code> 對象,該對象會被 fork 的子程序列印出來。然後 fork 的子程序每秒會發送一個自增的計數值,該值會被父程序列印出來。

Node.js 子程式:你應該知道的一切

我們來用 <code>fork</code> 函數實作一個更實用的例子。

這裡有個 HTTP 伺服器處理兩個端點。一個端點(下面的 <code>/compute</code>)計算密集,會花好幾秒種完成。我們可以用一個長循環來模拟:

這段程式有個比較大的問題:當 <code>/compute</code> 端點被請求,伺服器不能處理其他請求,因為長循環導緻事件循環處于繁忙狀态。

這個問題有一些解決之道,這取決于耗時長運算的性質。但針對所有運算都适用的解決方法是,用 <code>fork</code> 将計算過程移動到另一個程序。

我們首先移動整個 <code>longComputation</code> 函數到它自己的檔案,并在主程序通過消息發出通知時,在檔案中調用這個函數:

一個新的 <code>compute.js</code> 檔案中:

現在,我們可以 <code>fork</code> <code>compute.js</code> 檔案,并用消息接口實作伺服器和複刻程序的消息通信,而不是在主程序事件循環中執行耗時操作。

上面的代碼中,當 <code>/compute</code> 來了一個請求,我們可以簡單地發送一條消息給複刻程序,來啟動執行耗時運算。主程序的事件循環并不會阻塞。

一旦複刻程序執行完耗時操作,它可以用 <code>process.send</code> 将結果發回給父程序。

在父程序中,我們在 fork 的子程序本身上監聽 <code>message</code> 事件。當該事件觸發,我們會得到一個準備好的 <code>sum</code> 值,并通過 HTTP 發送給請求。

上面的代碼,當然,我們可以 fork 的程序數是有限的。但執行這段代碼時,HTTP 請求耗時運算的端點,主伺服器根本不會阻塞,并且還可以接受更多的請求。

我的下篇文章的主題,<code>cluster</code> 子產品,正是基于子程序 fork 和負載均衡請求的思想,這些子程序來自大量的 fork,我們可以在任何系統中建立它們。

以上就是我針對這個話題要講的全部。感謝閱讀!下次再見!

<b>原文釋出時間為:2017年7月7日</b>

<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>