首頁>技術>

Nodejs作為一款優秀的JavaScript執行框架,被廣泛應用於網站的後臺開發中。得益於其單執行緒事件系統的工作原理,Nodejs非常善於處理高併發I/O請求。但相比於Java或者Go,Nodejs也因其單執行緒的特性,被認為不適合處理資源緊張型任務。

但是從版本12.0開始,Nodejs引入了穩定的多執行緒模組worker_threads,該模組允許nodejs程式在執行時新建獨立於主執行緒的子執行緒。透過這個模組,我們可以把那些複雜的任務放到一個單獨的執行緒裡執行,從而不會阻斷主執行緒的執行。下面讓我們來詳細瞭解下如何使用它。

Nodejs的工作原理

事件執行緒運行於V8,輔助執行緒運行於Libuv

具體介紹前,我們先來看看nodejs基本的工作原理。很多人以為Nodejs程式在執行時真的所有邏輯都在一個執行緒裡執行,其實這種理解是不準確的。總體上來說,Nodejs的執行執行緒大概可分為兩類,第一類是事件執行緒(event loop),另外一類叫做輔助執行緒。

事件執行緒用來接受程式中的事件並且在合適的時機執行這些事件的回撥。當一個回撥裡包含了複雜邏輯時,執行這個回撥就會阻塞事件執行緒來處理接下來的回撥。這時我們可以選擇把這些複雜邏輯再次拆分成多個非同步事件送到事件執行緒中擇機執行,但是這種方式治標不治本,事件執行緒仍然會在未來某個時刻被阻塞。

輔助執行緒是獨立於事件執行緒的一批執行緒,它們主要的責任是接受來自事件執行緒的特定任務並處理,然後把處理結果返回給事件執行緒。nodejs使用libuv來實現輔助執行緒。輔助執行緒主要負責與系統打交道的任務,比如fs模組以及crypto模組中大部分的實現都是在輔助執行緒中執行的。

儘管輔助執行緒幫助完成了很多資源緊張型的任務,但是使用者程式碼中的複雜邏輯卻無法使用輔助執行緒,仍然需要在事件執行緒中執行。這也就是worker_threads模組要解決的問題所在。

與cluster模組以及child_process的區別

實際上,Nodejs從很早的版本就支援使用cluster模組的fork方法來建立子程序,並在子程序中建立新的nodejs單執行緒執行例項。透過這種方式,我們可以在多個CPU核心中建立多個nodejs程序。

但需要注意的是,使用cluster實現的多執行緒模式並不是傳統意義上的多執行緒模式,因為每個執行緒都處於不同的程序中。這導致執行緒之間無法記憶體共享,並且建立程序的系統開銷也遠遠大於建立執行緒。

相比於cluster,worker_threads可以在同一程序中建立輕量級的執行緒,並且允許執行緒間共享記憶體。

worker_threads模組

worker_threads模組允許我們建立真正意義上的多執行緒nodejs程式 。透過初始化模組中的Worker類來建立一個新的worker執行緒(為了表述方便,下面的文章用worker代指worker執行緒)。

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');// 新建worker執行緒並載入執行程式碼檔案const worker = new Worker('./worker_script.js');worker.on('message', msg => console.log(msg));worker.on('error', err => console.error(err));worker.on('exit', () => console.log('finished'));// 新建worker執行緒並解析傳入字串作為執行程式碼const worker2 = new Worker(  `const { parentPort } = require("worker_threads"); parentPort.postMessage("hi")`,  { eval: true });worker2.on('message', msg => console.log(msg));worker2.on('error', err => console.error(err));worker2.on('exit', () => console.log('finished'));

可以看到, 上面有兩種方式來載入執行緒的執行程式碼,一種是提供一個程式碼檔案的相對路徑或者絕對路徑,另外一種是直接傳入一段字串,並設定eval: true.

Worker類繼承於EventEmitter,所以它也暴露了以下生命週期事件可以用來監聽

“error” worker發生錯誤異常退出

“exit” worker正常退出

“message” 收到來自worker的資訊

“messageerror” worker資訊序列化失敗

“online” worker初始化完成

Worker執行緒的兩種執行方式以及執行緒池

Worker執行緒可分為單次執行執行緒或者常駐執行緒。比如下面是一個單次執行的worker執行緒。

const { workerData, parentPort } = require('worker_threads');function process(data) {  // ...}const results = workerData.items.map(item => process(item));parentPort.postMessage(results);

當worker處理完資料並把結果傳回給父執行緒後,worker執行緒就會退出並銷燬。再來看看實現相同邏輯的常駐執行緒

const { parentPort } = require('worker_threads');function process(data) {  // ...}parentPort.on('message', (items) => {  const results = items.map(item => process(item));  parentPort.postMessage(results);});

當常駐執行緒建立後,它會建立一個監聽器來接收來自主父執行緒的訊息並在接收到訊息後處理相關邏輯。相比於單次執行的執行緒,常駐執行緒省去了反覆初始化的資源浪費。並且我們可以以此來實現執行緒池。

執行緒間資料通訊

Nodejs的執行緒間使用通道(message channel)來進行通訊,每個通道有兩個埠(port),當一個執行緒被初始化後,父執行緒與子執行緒間會設定一個預設通道,我們可以在子執行緒中使用worker_threads模組的parentPort來引用預設通道的父埠。

儘管使用預設通道相對方便,但這不符合關注點分離的程式設計思想。所以更建議使用自定義通道來進行執行緒間通訊

/** *  父執行緒程式碼 **/const { Worker, MessageChannel } = require('worker_threads');const worker = new Worker('./script.js');// 建立通道,通道例項中的port1,port2屬性代表了通道的兩個埠const { port1, port2 } = new MessageChannel();// 使用自定義通道來接受子執行緒訊息port1.on('message', (message) => { console.log('message from worker:', message);});// 將port2傳給子執行緒worker.postMessage({ port2 }, [port2]);// 使用自定義通道向子執行緒發訊息port1.postMessage('data');/** *  子執行緒程式碼 **/const { parentPort, MessagePort } = require('worker_threads');// 使用預設通道接受自定義通道埠parentPort.on('message', (data) => {  const { port2 } = data;  // 使用自定義通道向父程序傳送訊息  port2.postMessage('data');});
執行緒間記憶體共享

執行緒間除了透過訊息的方式傳遞資料外,還支援共享記憶體的方式直接訪問資料。相比於訊息傳遞的方式,記憶體共享方式省去了對資料的序列化以及反序列化步驟,所以速度更快佔用系統資源也更小,但需要注意多個執行緒如果對同一記憶體中的資料進行修改,有可能造成資料的不一致性。

目前Nodejs支援透過SharedBuffer或者SharedArrayBuffer類來傳遞記憶體共享資料,但這兩個類只支援對基本型別的資料或者基本型別資料陣列。所以目前來看,如果要傳遞複雜結構的資料,還是避免不了序列化與反序列化。

39
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Python傳參工具argparse實現可選引數與預設引數值