js执行完一个函数再执行另一个_理解 Node.js 中的 Worker Threads

本文译自 Liz Parody 发表在 NodeSource 的文章 Understanding Worker Threads in Node.js

要想理解 Worker(工作线程)的概念,就需要先理解 Node.js 的构成:

  • 一个进程:它是一个你可以在任何地方访问的全局对象,其中有一些与本次进程执行相关的信息

  • 一个线程:单线程意味着在给定进程中同一时间内只执行一组指令

  • 一个事件循环:这是 Node.js 最重要的部分之一,也是允许 Node 支持异步非阻塞 I/O 的原因,尽管 JavaScript 是单线程的,但却可以通过回调、Promises 和 async/await 来尽可能将操作卸载(offloading)到系统内核

  • 一个 JS 引擎实例:执行 JavaScript 代码的程序

  • 一个 Node.js 实例:执行 Node.js 代码的程序

换句话说,Node 执行在一个单线程上,并且同一时间,事件循环上只有一个进程、一份代码、一次执行(代码无法并行执行)。这一点非常有用,因为它简化了编写 JavaScript 代码的过程,因为你不需要考虑并发问题。

之所以它是以这种方式构建的,是因为最初 JavaScript 是为客户端交互而生的(如网页交互和表单验证等),这些都不需要复杂的多线程模型。

但是与所有其他的事情一样,这也带来了问题:如果你要执行一段 CPU 密集型的代码,比如在内存中的大数据集上进行复杂计算,这会阻塞其他要被执行的代码。同样的,如果你请求一个服务端的 CPU 密集型 API,这段代码将阻塞事件循环并使得该服务无法响应其他的请求。

如果主事件循环必须等待一个函数执行完才能再执行下一条指令,则这个函数就是阻塞的。而非阻塞的函数可以允许主事件循环在函数开始执行后继续执行后续的代码,直到该函数执行完成并通知主事件循环去执行回调。

黄金法则:不要阻塞事件循环,尽可能保持它持续运转,留意并避免任何可能阻塞线程的代码,比如同步的网络调用以及大量的同步循环。

对开发者来说非常重要的一点是要能够区分哪些是 CPU 操作,哪些是 I/O 操作。就像之前提到的,Node.js 的代码不会并行执行,但 I/O 操作可以并发执行,他们都是异步的。因此工作线程(Worker Threads)不会对 I/O 密集型的代码带来什么提升,其主要还是提升 CPU 密集型操作的性能。

解决方案

目前已经有了一些 CPU 密集型操作的解决方案:用多个进程(cluster 模块)来提高系统中的 CPU 的利用率。

这种方案的优势很明显,它可以隔离进程,因此一个进程中出现任何问题都不会影响到其他进程。但是,这也意味着他们之间无法共享内存,数据通信也必须通过 JSON 序列化。

人们开始设想在 Node.js 中加入一个新的模块来支持创建线程,并实现他们之间的同步,这就解决了 CPU 密集计算的问题。然而现实将并不是这样,如果添加了多线程的模型,将会直接改变 JavaScritp 这门语言的性质,而不是简单的添加了几个类几个函数就可以的。

最佳解决方案

CPU 性能的最佳解决方案就是工作线程,其实浏览器在很久之前就已经采用了这种方案。

与之前提到的 Node.js 的构成,即一个进程 + 一个线程 + 一个事件循环 + 一个 JS 引擎 + 一个 Node.js 实例所不同,工作线程的方式采用的是一个进程 + 多个线程 + 每个线程一个时间循环 + 每个线程一个 JS 引擎 + 每个线程一个 Node.js 实例。大家可以参考下面示意图的对比:

3b831fd88280017aed2ac6de45823485.png

Node 中的 worker_threads 模块提供了同时运行多个 JavaScript 线程的能力。可以通过以下方式引入:

const worker = require('worker_threads');

从 Node.js 10 开始就已经引入了工作线程,但它仍然处于实验阶段, 需要开启 --experimental-worker 选项(Node.js 12 无需开启选项即可使用)。

理想的是在同一进程中有多个 Node.js 实例。

在使用工作线程时,一个线程的结束并不影响主进程,而当工作线程结束后,该线程分配的系统资源如果被 Hang 住,这将会导致出现内存泄露,我们并不希望如此。

我们所希望的是将 Node.js 嵌入其中,让 Node.js 有能力去创建新的线程以及在其中创建新的 Node.js 实例,他们都在同一个进程中的独立线程上运行。

工作线程相关的概念如下:

  • ArrayBuffers: 可用于将一个线程的内存数据传递给另一个线程

  • SharedArrayBuffer: 可以用于在各个线程之间共享同一块内存区域的数据(仅限二进制数据)

  • Atomics: 用来支持线程间的并发操作,是一种更有效的实现 JavaScript 多线程间的条件变量的手段

  • MessagePort: 用来在不同的线程间通信, 可以传输结构化的数据,内存区域甚至其他的 MessagePorts

  • MessageChannel: 是一个异步双端的通信通道,可以用于两个线程间的彼此通信

  • WorkerData: 用来传递工作线程启动参数。该参数可以是任意 JavaScript 值,它将被克隆后再传递给工作线程,此外 postMessage() 也会将参数进行克隆

工作线程提供的主要 API 如下:

  • const { Worker, parentPort } = require('worker_threads') Worker 类表示一个独立的 JavaScript 执行线程,parentPort 是一个 MessagePort 的实例

  • new Worker(filename) 或 new Worker(code, { eval: true }): 这是启动一个工作线程的两种主要方式,支持传递一个文件路径或直接传递你想要执行的代码,通常建议采用文件的方式

  • worker.on('message'):父线程监听工作线程发过来的消息

  • worker.postMessage(data):父线程向工作线程发送消息

  • parentPort.on('message'):工作线程监听父线程发来的消息

  • parentPort.postMessage(data): 工作线程监听父线程的消息

示例

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() 创建了一个新工作线程,该线程的代码监听了父线程的消息,一旦收到消息则向父线程再回送该消息本身。

如果你用的是 Node.js 10, 别忘了加上 --experimental-worker 选项

对工作线程的一些合理期望:

  • 支持对原生句柄的传递(比如 sockets,http 请求等)

  • 死锁检测。死锁是一组线程中,每个都持有一部分资源并等待其他线程持有的另一部分资源。当出现这种情况时,死锁检测将会是很有用的特性

  • 更好的隔离。如果一个线程受到影响,它不应该影响到其他执行中的线程

对工作线程的不合理期望:

  • 不要指望工作线程可以带来不可思议的性能提升,有时候自己管理一个进程池也许会更好

  • 不要使用工作线程来执行并行化的 I/O 操作

  • 不要认为创建工作线程的开销很低

此外,Chrome DevTools 是可以支持 Node.js 中的工作线程的。工作线程是 Node.js 中一个很有前途的实验模块,如果你的 Node 程序中需要处理 CPU 密集型任务,目前还不建议在生产环境中使用工作线程,你可以考虑使用进程池的方式来替代。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值