Node.js多线程:什么是Worker线程?它们为什么如此重要?(下篇)

原文链接:

后台进程

因此,对于一些简单的使用情况,setImmediate()也许还行,但是它远非理想的解决方案。另外,出于充分的理由,我们没有线程,也不想修改语言。

我们可以在没有线程的情况下进行并行处理吗?答案是可以的,我们需要的是某种后台处理方式,一种用于对输入执行任务的方式,他可以使用所需要的任意CPU和时间,并将结果返回给主应用程序。像这样:

// Runs `script.js` in a new environment without sharing memory.
const service = createService('script.js')
// We send an input and receive an output
service.compute(data, function(err, result) {
  // result available here
})

现实情况是,我们已经可以在Node.js中进行后台处理了:我们可以fork这个进程,并使用消息来传递数据。 主进程可以通过发送和接收事件与子进程进行通信。

没有共享内存。交换的所有数据都是“克隆的”,这意味着在一侧对数据更改,它不会在另一侧改变。 就像HTTP响应一样,发送后,另一端也只有它的副本。如果我们不共享内存,那么我们就没有竞争条件,也不需要线程。问题解决了!

好吧,等等。 这是一个解决方案,但不是理想的解决方案。 就资源而言,fork进程又昂贵又缓慢。这意味着由于进程之间不共享内存,因此需要从头开始使用大量内存创建并运行新的虚拟机。

我们可以重用同一个fork得到的进程吗? 当然可以,但是,在fork进程中,发送同步执行的不同繁重工作负载会产生两个问题。

一方面,您可能不会阻塞主应用程序,但是fork进程一次只能处理一项任务。如果您有两个任务,一个将花费10秒,另一个将花费1秒,按照顺序执行,不理想的是必须等待10秒才能执行第二个任务。

因为我们使用了fork进程,我们希望利用上操作系统对计算机中所有核心的调度。就像您可以同时听音乐和浏览互联网一样,您可以fork两个进程并同时执行所有任务。

另一方面,如果一个任务导致了进程崩溃,那么所有发送给同一进程的任务都将无法完成。

为了解决这些问题,我们需要多个fork进程,而不仅仅是一个。但是,我们需要限制fork进程的数量,因为每个进程都将在内存中复制所有虚拟机代码,这意味着每个进程都将占用好几MB的内存,并且启动时间不短。

因此,就像数据库连接一样,我们需要准备好要使用的进程池,在每个进程中一次运行一个任务,并在任务完成后重用该进程。实现看起来很复杂,而且也确实如此!让我们使用worker-farm来帮助我们:

// main app
const workerFarm = require('worker-farm')
const service = workerFarm(require.resolve('./script'))
 
service('hello', function (err, output) {
  console.log(output)
})

// script.js
// This will run in forked processes
module.exports = (input, callback) => {
  callback(null, input + ' ' + world)
}

问题解决了?

那么,问题解决了吗?是的,我们已经解决了问题,但与多线程解决方案相比,我们仍然使用了更多的内存。与fork进程相比,线程在资源方面非常轻量级。这就是Worker线程诞生的原因!

Worker线程具有独立的上下文。它们使用消息传递与主进程交换信息,因此我们避免了线程出现竞争的情况!但它们确实处于同一进程中,因此它们使用的内存要少得多。

您可以与Worker线程共享内存。您可以在Worker线程之间传递专门用于此目的的SharedArrayBuffer对象。并且仅在需要处理大量数据的CPU密集型任务时才使用它们,它们可以使您可以避免数据的序列化步骤。

让我们开始使用Worker线程!

如果您运行Node.js v10.5.0或更高版本,则可以立即开始使用worker_threads模块。但是,如果您使用的是11.7.0之前的任何版本,则需要在调用Node.js时使用--experimental-worker标志来启用它。

另外,请记住,创建一个Worker线程,尽管它比fork进程要轻量得多,也可能会使用过多的资源,这具体取决于您的需求。在这种情况下,worker_threads文档建议您创建一个Worker池。您也可以在npm中查找通用或特定的线程池实现,而不是创建自己的实现。

让我们看一个简单的例子。首先,我们需要实现主文件,在该文件中,我们将创建一个Worker线程并为其提供一些数据。该API是事件驱动的,但我们将其包装到Promise中,该Promise可解析从Worker收到的第一条消息:

// index.js
// run with node --experimental-worker index.js on Node.js 10.x
const { Worker } = require('worker_threads')

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

async function run() {
  const result = await runService('world')
  console.log(result);
}

run().catch(err => console.error(err))

如您所见,这就像传递文件名作为参数,以及我们希望Worker处理的数据一样容易。请记住,workerData数据是克隆的,并且不在任何共享内存中。然后,我们通过监听message事件等待Worker线程向我们发送消息。

现在我们需要实现服务:

const { workerData, parentPort } = require('worker_threads')
	
// You can do any heavy stuff here, in a synchronous way
// without blocking the "main thread"
parentPort.postMessage({ hello: workerData })

在这里,我们需要做两件事:主应用程序发送给我们的workerData,以及将信息返回到主应用程序的方法。这是通过拥有postMessage方法的parentPort完成的,我们将通过该方法传递处理结果。

这是最简单的示例,但是我们可以构建更复杂的东西,例如,如果需要提供反馈,则可以从Worker线程发送多个消息,指示执行状态。或者我们可以发送一部分结果。想象一下,您正在处理成千上万的图像。也许您想为每个图像处理发送一条消息,但您不想等到所有图像都处理完之后再发送。

为了运行该示例,如果您使用的是Node.js 11.7之前的任何版本,请记住使用--experimental-worker标志:

node --experimental-worker index.js

有关其他信息,请查看worker_threads文档。


利用多线程部署新的Web应用程序或网站很容易。确保一切都会继续为您的应用程序提供资源,这将使事情变得更加艰难。如果您希望确保成功完成对后端或第三方服务的请求,请尝试LogRocket

LogRocket就像Web应用程序的DVR,实际上记录了您网站上发生的一切。无需猜测问题发生的原因,您可以汇总并报告有问题的网络请求,以快速了解根本原因。

LogRocket用您的应用程序记录基线性能计时,例如页面加载时间,到第一个字节的时间和缓慢的网络请求,并记录Redux,NgRx和Vuex操作/状态。免费开始监视。

Web Workers呢?

也许您听说过Web Workers API。它们是用于网络的更成熟的API,并得到现代浏览器的良好支持。API的不同是因为需求和技术条件不同,但是它们可以解决浏览器运行时中的类似问题。如果您要在Web应用程序中进行加密、压缩/解压缩、图像处理、计算机视觉(例如,面部识别)等,此功能将非常有用。

结论

如果您需要在Node.js应用程序中执行CPU密集型任务,则Worker线程是一个令人兴奋且有用的模块。就像没有共享内存的线程,因此,它们没有引入潜在的竞争条件。由于worker_threads模块在Node.js v12 LTS中变得稳定,因此在生产级应用程序中使用它应该会感到放心!

(完)


今天的分享就到这里了,感谢各位读者的阅读。如您对小编分享的文章感兴趣,您可以在文章下方点赞、分享或收藏;还可以通过搜索关注知乎专栏《微服务应用开发和API管理》,或者扫描下方二维码关注灵长科技官方公众号,获取最新更新动态。

weixin.qq.com/r/KzkQCMP (二维码自动识别)

未经同意,本文禁止转载或摘编。

发布于 2020-01-21 19:16