学习使用Node.js工作线程

76 阅读3分钟

开始使用Node.js工作线程

众所周知,Node.js是单线程的,允许在给定时间内执行单个命令。例如,执行处理器密集型的服务器端代码可能会阻塞事件循环,减缓其他后续请求的执行。

简介

为了解决这个问题,Node.js v10.5中引入了工作线程模块。

在本教程中,我们将了解工人线程的概念,它是如何工作的,以及它将如何帮助我们执行CPU密集型任务而不阻塞其他请求。

前提条件

  1. 在你的开发环境中安装Node.js
  2. JavaScript同步和异步编程的基础知识。
  3. 对Node.js的工作原理有充分的了解。

学习目标

在本期Node.js工作线程课程结束时,你应该能够。

  • 理解Node.js工作线程的工作原理。
  • 使用工人线程。
  • 充分发挥工人线程的作用。
  • 理解工人线程池的概念。

探索Node.js中的工人线程API

Node.js自带worker_threads 模块。这个模块有助于并行地运行JavaScript代码。
工作者线程负责通过传输ArrayBuffer 实例来处理CPU密集型的任务。

由于以下特点,它们已被证明是CPU性能的最佳解决方案。

  1. 它们用多个线程运行一个进程。
  2. 每个线程执行一个事件循环。
  3. 每个线程运行单个JS引擎实例。
  4. 每个线程执行单个Node.js实例。

工人线程如何工作

工人线程通过执行主线程指定的一段代码来工作。每个工作者都在与其他工作者隔离的情况下执行。但是,这些工作者可以根据需要通过消息通道来回传递消息。

父工作者使用worker.postMessage() 将消息写入通道,而子工作者使用parentPort.postMessage() 函数。

由于JavaScript不支持并发,Node.js工作者利用V8,允许工作者与其他现有工作者完全隔离地运行。

使用工作者线程

在本节中,让我们创建一个工作者线程的例子,并将其传递给一些假数据。

//add this code snippet to main.js

const { Worker } = require('worker_threads')

const runService = (WorkerData) => {
    return new Promise((resolve, reject) => {
    
        // import workerExample.js script..
    
        const worker = new Worker('./workerExample.js', { WorkerData });
        worker.on('message', resolve);
        worker.on('error', reject);
        worker.on('exit', (code) => {
            if (code !== 0)
                reject(new Error(`stopped with  ${code} exit code`));
        })
    })
}

const run = async () => {
    const result = await runService('hello John Doe')
    console.log(result);
}

run().catch(err => console.error(err))
// add this to workerExample.js file.
const { WorkerData, parentPort } = require('worker_threads')
parentPort.postMessage({ welcome: WorkerData })

输出


{ welcome: 'hello John Doe' }

在我们的main.js 脚本中,我们首先从worker_threads 导入Worker ,然后传递data(filename) 供工作者处理。

下一步是监听来自工人线程的消息事件,正如在workerExample.js 服务中看到的那样。

这个工人服务拥有从我们的主应用程序发送的WorkerData ,以及通过parentPort 发送回已处理数据的方法。这个对象(parentPort )有postMessage() ,我们用它来传递处理过的数据。

工作者线程也提供了共享内存的方法,使用我们之前看到的SharedArrayBuffer instances 。记住共享内存也可以通过传输ArrayBuffer instances

创建和执行新的工作者

在这一节中,让我们看看CPU密集型的例子,生成一个斐波那契数列。

这个任务,如果在没有工作者线程的情况下生成,随着nth ,主线程会被阻塞。

//
In your `index.js` file, add the following:
const {Worker} = require("worker_threads");

let number = 10;

const worker = new Worker("./myWorker.js", {workerData: {num: number}});

worker.once("message", result => {
    console.log(`${number}th Fibonacci No: ${result}`);
});

worker.on("error", error => {
    console.log(error);
});

worker.on("exit", exitCode => {
    console.log(`It exited with code ${exitCode}`);
})

console.log("Execution in main thread");
//add this script in myWorker.js file
const {parentPort, workerData} = require("worker_threads");

parentPort.postMessage(getFibonacciNumber(workerData.num))

function getFibonacciNumber(num) {
    if (num === 0) {
        return 0;
    }
    else if (num === 1) {
        return 1;
    }
    else {
        return getFibonacciNumber(num - 1) + getFibonacciNumber(num - 2);
    }
}

输出

Execution in the main thread
10th Fibonacci No: 55
It exited with code 0

index.js 文件中,我们从Worker 类的一个实例中创建了一个工作线程,正如我们在前面的例子中看到的那样。

为了获得结果,我们监听3个事件。

  • message 当工作者发布消息时执行的事件。
  • exit 在工作者停止执行时被触发的事件。
  • error 是在发生错误时触发的。

我们在index.js 中的最后一行。

console.log("Execution in main thread");

这是在主线程中的执行,在我们等待worker的结果时,将执行该执行,如上面的输出所见。

因此,只要在工作者线程中处理一个CPU密集型任务,我们就可以处理任何其他任务,而不必担心阻塞主线程。

总结

由于Node.ja在处理CPU密集型任务时的性能问题,它一直被人诟病。工作线程的引入有效地解决了这些缺点,从而提高了Node.js的能力。