[译]理解 Node.js 的中 Worker Threads

原文:nodesource.com/blog/worker…node

理解 Node 的底层对于理解 Workers 是颇有必要的。web

当一个 Node.js 的应用启动的同时,它会启动以下模块:编程

  • 一个进程
  • 一个线程
  • 事件循环机制
  • JS 引擎实例
  • Node.js 实例

一个进程:process 对象是一个全局变量,可在 Node.js 程序中任意地方访问,并提供当前进程的相关信息。promise

一个线程:单线程意味着在当前进程中同一时刻只有一个指令在执行。浏览器

事件循环:这是 Node.js 中须要重点理解的一个部分,尽管 JavaScript 是单线程的,但经过使用回调,promises, async/await 等语法,基于事件循环将对操做系统的操做异步化,使得 Node 拥有异步非阻塞 IO 的特性。bash

一个 JS 引擎实例:即一个能够运行 JavaScript 代码的程序。网络

一个 Node.js 实例:即一个能够运行 Node.js 环境的程序。多线程

换言之,Node 运行在单线程上,而且在事件循环中同一时刻只有一个进程的任务被执行,每次同一时刻只会执行一段代码(多段代码不会同时执行)。这是很是有效的,由于这样的机制足够简单,让你在使用 JavaScript 的时候无需担忧并发编程的问题。并发

这样的缘由在于 JavaScript 起初是用于客户端的交互(好比 web 页面的交互或表单的验证),这些逻辑并不须要多线程这样的机制来处理。异步

因此这也带来了另外一个缺点:若是你须要使用 CPU 密集型的任务,好比在内存中使用一个大的数据集进行复杂计算,它会阻塞掉其余进程的任务。一样的,当你在发起一个有 CPU 密集型任务的远程接口请求时,也一样会阻塞掉其余须要被执行的请求。

若是一个函数阻塞了事件循环机制直到这个函数执行完才能执行下一个函数,那么它就被认为是一个阻塞型函数。一个非阻塞的函数是不会阻塞住事件循环进行下一个函数的执行的,它会使用回调通知事件循环函数任务已执行完毕。

最佳实践:不要阻塞事件循环,要让事件循环保持不断运行,而且注意避免使用回阻塞线程的操做好比同步的网络接口调用或死循环。

区分开 CPU 密集型操做与 I/O(input/output) 密集型操做是很重要的。像前面所说的,Node.js 并不会同时执行多段代码,只有 I/O 操做才会同时去执行,由于它们是异步的。

因此 Worker Threads 对于 I/O 密集型操做是没有太大的帮助的,由于异步的 I/O 操做比 worker 更有效率,Wokers 的主要做用是用于提高对于 CPU 密集型操做的性能。

其余方案

此外,目前已经存在不少对于 CPU 密集型操做的解决方案,好比多进程(cluster API)方案,保证了充分利用多核 CPU。

这个方案的好处在于进程之间是相互独立的,若是一个进程出现了问题,并不会影响到其余进程。此外它们还拥有稳定的 API,然而,这也意味着不能同享内存空间,并且进程间通讯只能经过 JSON 格式的数据进行交互。

JavaScript 和 Node.js 不会有多线程,理由以下:

因此,人们可能会认为添加一个建立和同步线程的 Node.js 核心模块就能够解决 CPU 密集型操做的需求。

然而并非,若是添加多线程模块,将会改变语言自己的特性。添加多线程模块做为可用的类或者函数是不可能的。在一些支持多线程的语言好比 Java 中,使用同步特性来使得多个线程之间的同步可以实现。

而且一些数字类型是不够原子性的,这意味着若是你不一样步操做它们,在多线程的同时执行计算的状况下,变量的值可能会不断变更,没有肯定的值,变量的值可能通过一个线程计算后改变了几个字节,在另外一个线程计算后有改变了其余几个字节的数据。好比,在 JavaScript 中一些简单的计算像 0.1 + 0.2 的结果中小数部分有 17 位(小数的最高位数)。

var x = 0.1 + 0.2; // x will be 0.30000000000000004
复制代码

可是浮点数的计算并非 100% 精准的。因此若是不一样步计算,小数部分的数字就会由于多个线程永远没有一个准确的数字。

最佳实践

因此解决 CPU 密集型操做的性能问题是使用 Worker Threads。浏览器在好久以前就已经有了 Workers 特性了。

单线程下的 Node.js:

  • 一个进程
  • 一个线程
  • 一个事件循环
  • 一个 JS 引擎实例
  • 一个 Node.js 实例

多线程 Workers 下 Node.js 拥有:

  • 一个进程
  • 多个线程
  • 每一个线程都拥有独立的事件循环
  • 每一个线程都拥有一个 JS 引擎实例
  • 每一个线程都拥有一个 Node.js 实例

就像下图:

Worker_threads 模块容许使用多个线程来同时执行 JavaScript 代码。使用下面这个方式引入:

const worker = require('worker_threads');
复制代码

Worker Threads 已经被添加到 Node.js 10 版本中,可是仍处于实验阶段。

使用 Worker threads 咱们能够在在同一个进程内能够拥有多个 Node.js 实例,而且线程能够不须要跟随父进程的终止的时候才被终止,它能够在任意时刻被终止。当 Worker 线程销毁的时候分配给该 Worker 线程的资源依然没有被释放是一个很很差的操做,这会致使内存泄漏问题,咱们也不但愿这样。咱们但愿这些分配资源可以嵌入到 Node.js 中,让 Node.js 有建立线程的能力,而且在线程中建立一个新的 Node.js 实例,本质上就像是在同一个进程中运行多个独立的线程。

Worker Threads 有以下特性:

  • ArrayBuffers 能够将内存中的变量从一个线程转到另一个
  • SharedArrayBuffer 能够在多个线程中共享内存中的变量,可是限制为二进制格式的数据。
  • 可用的原子操做,可让你更有效率地同时执行某些操做而且实现竞态变量
  • 消息端口,用于多个线程间通讯。能够用于多个线程间传输结构化的数据,内存空间
  • 消息通道就像多线程间的一个异步的双向通讯通道。
  • WorkerData 是用于传输启动数据。在多个线程间使用 postMessgae 进行传输的时候,数据会被克隆,并将克隆的数据传输到线程的 contructor 中。

API:

  • const { worker, parantPort } = require('worker_threads'); =>worker 函数至关于一个独立的 JavaScript 运行环境线程,parentPort 是消息端口的一个实例
  • new Worker(filename) or new Worker(code, { eval: true }) =>启动 worker 的时候有两种方式,能够经过传输文件路径或者代码,在生产环境中推荐使用文件路径的方式。
  • worker.on('message'),worker.postMessage(data) => 这是多线程间监听事件与推送数据的方式。
  • parentPort.on('message'), parentPort.postMessage(data) => 在线程中使用 parentPort.postMessage 方式推送的数据能够在父进程中使用 worker.on('message') 的方式接收到,在父进程中使用 worker.postMessage() 的方式推送的数据能够在线程中使用 parentPort.on('message') 的方式监听到。

例子

const { Worker } = require('worker_threads');

const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.once('message',
    message => parentPort.postMessage({ pong: message }));  
`, { eval: true });
worker.on('message', message => console.log(message));      
worker.postMessage('ping');
复制代码
$ node --experimental-worker test.js
{ pong: ‘ping’ }
复制代码

上面例子所作的也就是使用 new Worker 建立一个线程,线程中的代码监听了 parentPort 的消息,而且当接收到数据的时候只触发一次回调,将收到的数据传输回父进程中。

你须要使用 --experimental-worker 启动程序由于 Workers 还在实验阶段。

另外一个例子:

const {
	Worker, isMainThread, parentPort, workerData
} = require('worker_threads');

if (isMainThread) {
    module.exports = function parseJSAsync(script) {
        return new Promise((resolve, reject) => {
        	const worker = new Worker(filename, {
        		workerData: script
    		});
            worker.on('message', resolve);
            worker.on('error', reject);
            worker.on('exit', (code) => {
                if (code !== 0)
                    reject(new Error(`Worker stopped with exit code ${code}`));
            });
         });
    };
} else {
    const { parse } = require('some-js-parsing-library');
    const script = workerData;
    parentPort.postMessage(parse(script));
}
复制代码

上面代码中:

  • Worker: 至关于一个独立的 JavaScirpt 运行线程。
  • isMainThread: 若是为 true 的话说明代码不是运行在 Worker 线程中
  • parentPort: 消息端口被使用来进行线程间通讯
  • workerData:被传入 worker 的 contructor 的克隆数据。

在实际使用中,应该使用线程池的方式,否则不断地建立 worker 线程的代价将会超过它带来的好处。

对于 Worker 的使用建议:

  • 传输原生的句柄好比 sockets,http 请求
  • 死锁检测。死锁是一种多个进程间被阻塞的状况,缘由是每个进程都持有一部分资源并等待另外一个进程释放它所持有的资源。在 Workers Threads 中死锁检测是很是有用的特性
  • 更好的隔离,因此若是一个线程中受影响,它不会影响到其余线程。

对于 Worker 的一些很差的想法:

  • 不要认为 Workers 会带来难以想象的速度提高,有时候使用线程池会是更好的选择。
  • 不要使用 Workers 来并行执行 I/O 操做。
  • 不要认为建立 Worker 进程的开销是很低的。

最后

Chrome devTools 支持 Node.js 中的 Workers 线程特性。worker_threads 是一个实验模块,若是你须要在 Node.js 中运行 CPU 密集型的操做,目前不建议在生产环境中使用 worker 线程,可使用进程池的方式来代替。


关注【IVWEB社区】公众号获取每周最新文章,通往人生之巅!

相关文章
相关标签/搜索