如何在 Node.js 中管理多线程
在 YouTube 上观看视频
子进程、集群和工作线程
工作池
使用工作池管理多个线程
测试工作池
结论
在本文中,我将向您展示如何通过管理多线程将Node应用程序的性能提升三倍。这是一个重要的教程,其中的方法和示例将为您提供设置生产环境线程管理所需的一切。
在 YouTube 上观看视频
子进程、集群和工作线程
长期以来,Node 一直具有多线程能力,通过使用子进程、集群或较新的首选模块方法(称为工作线程)。
子进程是为应用程序创建多线程的初始方式,自 0.10 版本起可用。这是通过为每个需要创建的额外线程生成一个节点进程来实现的。
集群功能自版本 4 左右以来一直处于稳定状态,它使我们能够简化子进程的创建和管理。它与PM2结合使用时效果极佳。
在我们开始对应用程序进行多线程处理之前,您需要充分理解以下几点:
1. I/O 任务已经存在多线程
Node 中有一个已经实现多线程的层,那就是libuv线程池。诸如文件和文件夹管理、TCP/UDP 事务、压缩和加密等 I/O 任务会被交给 libuv 处理,如果这些任务本质上不是异步的,则会在 libuv 的线程池中处理。
2. 子进程/工作线程仅适用于同步 JavaScript 逻辑
使用子进程或工作线程实现多线程仅对执行繁重操作(例如循环、计算等)的同步 JavaScript 代码有效。如果您尝试将 I/O 任务卸载到工作线程,则不会看到性能提升。
3. 创建一个线程很容易,动态管理多个线程却很难
在应用中创建一个额外的线程很容易,因为有很多教程可以教你如何操作。然而,创建与你的机器或虚拟机正在运行的逻辑核心数量相当的线程,并管理这些线程之间的工作分配则要复杂得多,编写这样的逻辑代码超出了我们大多数人的能力范围😎。
谢天谢地,我们身处一个开源的世界,Node 社区贡献了众多杰出的贡献。这意味着,我们已经有一个模块可以让我们根据机器或虚拟机的 CPU 可用性动态创建和管理线程。
工作池
我们今天要使用的模块名为Worker Pool。Worker Pool 由Jos de Jong创建,它提供了一种创建工作线程池的简便方法,既可以动态卸载计算任务,也可以管理专用的工作线程池。它本质上是一个 Node.js 的线程池管理器,支持基于浏览器的 Worker 线程、子进程和 Web Worker 实现。
为了在我们的应用程序中使用 Worker Pool 模块,需要执行以下任务:
- 安装工作池
首先我们需要安装 Worker Pool 模块 - npm install workerpool
- 初始化工作池
接下来,我们需要在应用程序启动时初始化工作池
- 创建中间件层
然后,我们需要在重载 JavaScript 逻辑和管理它的工作池之间创建一个中间件层
- 更新现有逻辑
最后,我们需要更新我们的应用程序,以便在需要时将繁重的任务交给工作池
使用工作池管理多个线程
此时,您有 2 个选择:使用您自己的 NodeJS 应用程序(并安装workerpool和bcryptjs模块),或者从 GitHub 下载本教程和我的NodeJS 性能优化视频系列的源代码。
如果选择后者,本教程的文件将位于06-multithreading文件夹中。下载完成后,进入项目根文件夹并运行 npm install。之后,进入06-multithreading文件夹继续操作。
在worker-pool文件夹中,我们有两个文件:一个是工作池的控制器逻辑(controller.js)。另一个包含由线程触发的函数……也就是我之前提到的中间件层(thread-functions.js)。
工作池/控制器.js
'use strict'
const WorkerPool = require('workerpool')
const Path = require('path')
let poolProxy = null
// FUNCTIONS
const init = async (options) => {
const pool = WorkerPool.pool(Path.join(__dirname, './thread-functions.js'), options)
poolProxy = await pool.proxy()
console.log(`Worker Threads Enabled - Min Workers: ${pool.minWorkers} - Max Workers: ${pool.maxWorkers} - Worker Type: ${pool.workerType}`)
}
const get = () => {
return poolProxy
}
// EXPORTS
exports.init = init
exports.get = get
controller.js 是我们需要workerpool模块的地方。我们还导出了两个函数,分别是init和get。init函数会在应用程序加载期间执行一次。它会使用我们提供的选项以及对thread-functions.js的引用来实例化工作池。它还会创建一个代理,该代理会在应用程序运行期间一直保存在内存中。get函数只是返回内存中的代理。
工作池/线程函数.js
'use strict'
const WorkerPool = require('workerpool')
const Utilities = require('../2-utilities')
// MIDDLEWARE FUNCTIONS
const bcryptHash = (password) => {
return Utilities.bcryptHash(password)
}
// CREATE WORKERS
WorkerPool.worker({
bcryptHash
})
在thread-functions.js文件中,我们创建了由工作池管理的工作函数。在本例中,我们将使用BcryptJS对密码进行哈希处理。这通常需要大约 10 毫秒的运行时间,具体取决于用户机器的速度,这在处理高负载任务时非常实用。utilities.js 文件中包含了对密码进行哈希处理的函数和逻辑。我们在 thread-functions 中所做的就是通过 workerpool 函数执行这个bcryptHash函数。这使我们能够保持代码集中,避免重复或混淆某些操作的位置。
2-utilities.js
'use strict'
const BCrypt = require('bcryptjs')
const bcryptHash = async (password) => {
return await BCrypt.hash(password, 8)
}
exports.bcryptHash = bcryptHash
.env
NODE_ENV="production"
PORT=6000
WORKER_POOL_ENABLED="1"
.env 文件保存了端口号,并将NODE_ENV变量设置为“production”。我们还可以通过将WORKER_POOL_ENABLED设置为“1”或“0”来指定是否启用或禁用工作池。
1-app.js
'use strict'
require('dotenv').config()
const Express = require('express')
const App = Express()
const HTTP = require('http')
const Utilities = require('./2-utilities')
const WorkerCon = require('./worker-pool/controller')
// Router Setup
App.get('/bcrypt', async (req, res) => {
const password = 'This is a long password'
let result = null
let workerPool = null
if (process.env.WORKER_POOL_ENABLED === '1') {
workerPool = WorkerCon.get()
result = await workerPool.bcryptHash(password)
} else {
result = await Utilities.bcryptHash(password)
}
res.send(result)
})
// Server Setup
const port = process.env.PORT
const server = HTTP.createServer(App)
;(async () => {
// Init Worker Pool
if (process.env.WORKER_POOL_ENABLED === '1') {
const options = { minWorkers: 'max' }
await WorkerCon.init(options)
}
// Start Server
server.listen(port, () => {
console.log('NodeJS Performance Optimizations listening on: ', port)
})
})()
最后,我们的1-app.js 文件保存了应用启动时将执行的代码。首先,我们初始化.env文件中的变量。然后,我们设置一个Express服务器并创建一个名为/bcrypt的路由。当此路由触发时,我们将检查工作池是否已启用。如果启用,我们将获取工作池代理的句柄,并执行在thread-functions.js文件中声明的bcryptHash函数。这将依次执行Utilities中的bcryptHash函数并返回结果。如果工作池已禁用,我们只需直接在Utilities中执行bcryptHash函数即可。
在1-app.js的底部,你会看到我们有一个自调用函数。这样做是为了支持 async/await,我们在与工作线程池交互时会用到它。如果启用了工作线程池,我们会在这里初始化它。我们唯一需要覆盖的配置是将minWorkers设置为“max”。这将确保工作线程池生成的线程数量与机器上的逻辑核心数量相同,除了 1 个逻辑核心用于主线程之外。在我的例子中,我有 6 个物理核心(带超线程),这意味着我有 12 个逻辑核心。因此,将minWorkers设置为“max”后,工作线程池将创建并管理 11 个线程。最后,最后一段代码是我们启动服务器并监听 6000 端口。
测试工作池
测试工作池非常简单,只需启动应用程序,并在运行时向 发出 get 请求即可。如果您有AutoCannonhttp://localhost:6000/bcrypt
这样的负载测试工具,您可以轻松查看启用/禁用工作池时的性能差异。AutoCannon 非常易于使用。
结论
希望本教程能帮助您了解如何在 Node 应用程序中管理多线程。本文顶部嵌入的视频提供了 Node 应用测试的现场演示。
下次再见,欢呼 :)
文章来源:https://dev.to/bleedingcode/managing-multiple-threads-in-node-js-3mpc