明白Nodejs中的Worker-Threads

7次阅读

共计 4135 个字符,预计需要花费 11 分钟才能阅读完成。

原文

对于想了解,进程,线程,io 这些东西的朋友推荐个文章

想要明白 workers,首先需要明白 node 是怎样构成的。
当一个 node 进程开始,它其实是:

  1. 一个进程。
  2. 一个线程。
  3. 一个事件轮垂。
  4. 一个 js 引擎实例。
  5. 一个 node.js 实例。

一个进程:是指一个全局对象,这个对象能够访问任何地方,并且包含当前处理时的此时信息。

一个线程:单线程意味着单位时间内只有一组指令在给定的进程中执行。

一个事件轮垂:这是理解 Node 最重要的概念。它使 Node 更够异步以及拥有无锁定 I /O。即使 js 是单线程的,通过提供一些系统核心的操作像是回调函数,promise 函数以及异步的 async/await 函数这些功能。

一个 JS 引擎实例:这是个计算机程序,用来执行 js 的代码。

一个 Node.js 实例:一个计算机程序用来执行 node.js 的代码。

一句话,Node 运行在一个单线程上,每次事件轮垂只有一个进程存在。一个代码一次执行(不是并行执行)。这个非常重要,因为它很简单,你不用考虑并发的问题。

这么设计的原因是因为 js 生出来最初是用来开发客户端交互的(像是页面交互,表单这些),没有对线程这种用复杂的需求。

但是,和所有的事情一样,这样也有缺点:如果你有 cpu 敏感的代码,例如内存中有大量的来回计算的复杂数据,那么这能锁住其他需要进行处理计算的任务。像是,你向服务器发起一个请求,应对这个请求的接口有 cpu 敏感的代码,那么它就能锁定事件轮垂进而阻止其他请求的处理(笔者:其实就是其他请求就需要长时间排队甚至超时)。

如果主事件轮垂必须等待一个函数执行完成然后才能执行其他命令,那么这个函数就是“锁定中”。一个无锁定函数会允许主事件轮垂从开始就持续地运行,并且在其执行完成时通知主事件轮垂调用回调函数。

黄金准则:不要锁定事件轮垂,尽量关注和避免那些可能造成锁定的任务,像是同步网路调用或者无线死循环。

明白 cpu 操作和 i / o 操作是很重要的。如上所讲,Node 中的代码不能并行执行。只是 i / o 是并行,因为他们是异步执行的。

所以,worker 线程(以下我们会使用这个 node 特有的概念)不能提升多少 i / o 敏感的任务,因为异步 i / o 本身就比 worker 高效很多。worker 的主要任务是提升 cpu 敏感操作的性能。

已有的解决方案

此外,这里已经有一些应对 cpu 敏感处理的方案:多进程(例如,cluster API)来保证 cpu 最大被利用。

这个方法好处是允许每个进程间是独立的,如果某个线程出了问题,不会影响到其他的。他们稳定且相同的 api。然而,这意味着牺牲了内存共享,并且数据通信必须用 json(有额外开销,性能稍低)。

JavaScript 和 Node.js 是永远不会有多线程的。原因如下:

so, 有人或许会考虑给 node.js 添加一个新的模块来允许我们创建一个同步线程,以此来解决 cpu 敏感处理的问题。

然而,这不会实现的。如果添加一个线程,这个语言的本质就会发生变化。使用类或者函数添加一个线程作为新特性是不可能。在支持多线程的语言中(如 java),“synchronized”之类的关键字就能帮助实现多线程。

还有,一些数据不是原子的,意味着如果你不是同步处理他们,你可能的结果是在两个线程上都可以访问并更改这个值得变量,最后得到一个两个线程都对这个者进行了一些改变的无效的值。例如一个简单的 0.1+20.2 的操作,这个操作拥有 17 为小数。

因为小数点不是 100% 准确的,所以如果不是同步的,有一个整数可能使用 worker 之后得到一个非整数的数字。

最好的解决方案是

提高 cpu 性能的最好的方案是使用 worker 线程。浏览器很早既有了 worker 这个概念了。

使亿有的结构从:

一个进程

一个线程

一个事件轮垂

一个 JS 隐情实例

一个 Node.js 实例

变成:

一个进程

多个线程

每个线程一个事件轮垂

每个线程一个 JS 隐情实例

每个线程一个 Node.js 实例

worker_threads模块能够实现使用线程实现并行执行 js。

const worker = require('worker_threads');

Worker Theads 在 Node.10 时开始可以使用,但是一直处于实验状态,在 12.11.0 时,变成稳定版本。

这个方案的意思是,在一个进程中拥有多个 Node.js 的实例。在 worker threads 中,一个线程可以有一些节点,这个节点不必是父进程。worker 结束后还被分配着一些资源不是好的实践,这会导致内存泄漏。我们想把 node.js 整个的潜入其中,并且给与 Node.js 去创建新的现成的能力,然后在线程中创建一个新的 Node.js 实例。本质上是独立运行在一个进程中的线程中。

下面这些使 Worker Theads 与众不同:

ArrayBuffers在线程间传递内存。
SharedArrayBuffer每个线程都可访问,在线程间分享内存。(只限二进制数据)。
Atomics已可用,允许你并行执行一些处理,更高效且允许你在 js 中实现条件变量。
MessagePort,用来在不同线程间进行通信。可以用来传递结构数据,内存域,以及不同 Worker 之间的 MessagePort(对象)。
MessageChannel代表一个异步的,双向通信的频道,用来在不同的 (worker) 线程间通信。
WorkerData用来传递起始数据。任意 js 数据的复制版本会被传递到这个 Worker 的构造函数中。如果使用postMessage(),数据也会被复制。

接口 API

  • const {worker, parentPort} = require('worker_threads'),worker类表示一个独立执行 js 的线程,parentPort是一个 message port 的实例。
  • new Worker(filename)或者 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 来监听和接受一次性的 message 信息,接收到信息后也会发布一个 message 个猪线程。

在只支持实验性 worker thread 的 node 版本中你必须使用 --experimental-worker 命令行选项来执行代码。

其他例子:

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该类代表一个独立的 js 执行线程。
isMainThead一个布尔值,当前代码是否运行在 Worker 线程中。
parentPortMessagePort 对象,如果当前线程是个生成的 Worker 线程,则允许和父线程通信。
workerData一个可以传递给线程构造函数的任何 js 数据的的复制数据。

在实战中,上面的任务最好使用线程池来替代。否则,开销可能大于好处。

对 Worker 的期望是什么(希望是):

  • 传递本地处理任务。(passing native handles around)
  • 锁死检测。锁死是指一种情形,一系列进程被锁定,因为每个进程都把持了一些资源,而且每个线程又在等待其他线程所把持的资源释放然后获取。锁死检测在 worker thead 中比较有用。
  • 更多的隔离,所以一旦一个线程收到了影响,其他的没事。

对 Worker 不期望的是:

  • 不要认为 worker 会使所有的东西都很快速,有些情况下最好使用线程池。
  • 不要使用 worker 来进行 io 并行操作。
  • 不要认为衍生一个线程成本很低。

最后:

Workers 有 chrome 开发工具,可用来监视 Node.js 中的 workers。

正文完
 0