锋利的NodeJS之NodeJS多线程
最近刚好有朋友在问Node.js多线程的问题,我总结了一下,可以考虑使用源码包里面的worker_threads或者第三方的模块来实现。
首先明确一下多线程在Node.js中的概念,然后在聊聊worker_threads的用法。天生异步,真心强大。
- Node.js多线程概述
有人可能会说,Node.js虽然是单线程的,但是可以利用循环事件(Event Loop)l来实现并发执行任务。追究其本质,NodeJs实际上使用了两种不同的线程,一个是用于处理循环事件的主线程一个是工作池(Worker pool)里面的一些辅助线程。关于这两种线程主要功能和关系如图1所示。

图1 Node.js线程图
所以从本质上来讲,NodeJs并不是真正的原生多线程,而是利用循环事件来实现高效并发执行任务。要做到真正的多线程,需要依赖其他模块或者第三方库。
2. Worker_threads是Node.js官方推荐的实现真正多线程的模块,有官方技术团队进行长期维护。Worker_threads不需要单独安装,它位于Node.js源码中,具体位置是lib/worker_threads.js。worker_threads模块允许使用并行执行JavaScript的线程,使用也非常方便,只需要引入该模块即可,代码如下。
const worker = require('worker_threads');
与child_process或cluster不同,worker_threads可以共享内存。它们通过传输ArrayBuffer实例或共享SharedArrayBuffer实例来实现。
官网上给了一个完整的例子,如下所示。
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', message => console.log(message));
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0)
reject(new Error(`Worker stopped with exit code ${code}`));
});
});
};
} else {
const { parse } = require('som-parse-libary');
const script = workerData;
parentPort.postMessage(parse(script));
}
笔者对以上代码开始解析,重点概念如下所示:
Worker该类代表一个独立的js执行线程。
isMainThead一个布尔值,当前代码是否运行在Worker线程中。
parentPortMessagePort对象,如果当前线程是个生成的Worker线程,则允许和父线程通信。
workerData一个可以传递给线程构造函数的任何js数据的的复制数据。
Worker_theads还提供了很多实用的API,整理如下所示。
1.worker.getEnvironmentData(key)
可以获取环境变量,先使用setEnvironmentData来设置环境变量,然后再使用g
etEnvironmentData来获取。
举一个简单的例子,代码如下所示。
const {
Worker,
isMainThread,
setEnvironmentData,
getEnvironmentData,
} = require('worker_threads');
if (isMainThread) {
setEnvironmentData('Hi', 'Node.js!');
const worker = new Worker(__filename);
} else {
console.log(getEnvironmentData('Hi'));.
}
执行这段代码,可以在控制台打印出“Node.js”字符串。
- isMainThread
isMainThread可以用来判断该进程是不是主线程,如果是主线程,则返回true,否则返回false。下面编写一个嵌套worker的代码,用于展示。
const { Worker, isMainThread } = require('worker_threads');
if (isMainThread) {
console.log("This is a main thread\r\n");
// This re-loads the current file inside a Worker instance.
new Worker(__filename);
} else {
console.log('Inside Worker!');
console.log(isMainThread); // Prints 'false'.
}
- MessageChannel和相关用法
MessageChannel是worker_threads提供的一个双向异步的消息通信信道。下面这段代码就展示了两个MessagePort对象互相传递消息的过程,我们如果想主动结束某个Channel,那么可以使用close事件来完成。
const {MessageChannel} = require('worker_threads');
const {port1, port2} = new MessageChannel();
// port1给port2发送信息
port1.postMessage({carName: 'BYD'});
port2.on('message', (message) => {
console.log("I receive message is ", message);
})
// port2给port1发送信息
port2.postMessage({personality: "Brave"});
port1.on('message', (message) => {
console.log("I receive message is ", message);
});
运行上面的代码,可以在控制台看到如下输出:
I receive message is { personality: 'Brave' }
I receive message is { carName: 'BYD' }
port.on(‘message’)方法是利用被动等待的方式接收事件,如果想手动接收信息可以使用receiveMessageOnPort方法,指定从某个port接收消息,如下所示。
const { MessageChannel, receiveMessageOnPort } = require('worker_threads');
const {port1, port2} = new MessageChannel();
port1.postMessage({Name: "freePHP"});
let result = receiveMessageOnPort(port2);
console.log(result);
let result2 = receiveMessageOnPort(port2);
console.log(result2);
运行上面的代码,可以得到如下输出。
{ message: { Name: 'freePHP' } }
undefined
从结果可以看出,receiveMessageOnPort可以指定从另一个MessagePort对象获取消息,是一次消耗消息。
实际工作中,我们不可能只使用单个线程来完成任务,所以需要创建线程池来维护和管理worker thread对象。为了简化线程池的实现,假设只会传递一个woker脚本作为参数,具体实现如下所示。需要单独安装async_hooks模块,它用于异步加载资源。
const { AsyncResource } = require('async_hooks'); // 用于异步加载资源
const { EventEmitter } = require('events');
const path = require('path');
const { Worker } = require('worker_threads');
const kTaskInfo = Symbol('kTaskInfo');
const kWorkerFreedEvent = Symbol('kWorkerFreedEvent');
class WorkerPoolTaskInfo extends AsyncResource {
constructor(callback) {
super('WorkerPoolTaskInfo');
this.callback = callback;
}
done(err, result) {
this.runInAsyncScope(this.callback, null, err, result);
this.emitDestroy(); // 只会被执行一次
}
}
class WorkerPool extends EventEmitter {
constructor(numThreads) {
super();
this.numThreads = numThreads;
this.workers = [];
this.freeWorkers = [];
for (let i = 0; i < numThreads; i++)
this.addNewWorker();
}
/**
* 添加新的线程
*/
addNewWorker() {
const worker = new Worker(path.resolve(__dirname, 'task2.js'));
worker.on('message', (result) => {
// 如果成功状态,则将回调传给runTask方法,然后worker移除TaskInfo标记。
worker[kTaskInfo].done(null, result);
worker[kTaskInfo] = null;
//
this.freeWorkers.push(worker);
this.emit(kWorkerFreedEvent);
});
worker.on('error', (err) => {
// 报错后调用回调
if (worker[kTaskInfo])
worker[kTaskInfo].done(err, null);
else
this.emit('error', err);
// 移除一个worker,然后启动一个新的worker来代替当前的worker
this.workers.splice(this.workers.indexOf(worker), 1);
this.addNewWorker();
});
this.workers.push(worker);
this.freeWorkers.push(worker);
this.emit(kWorkerFreedEvent);
}
/**
* 执行任务
* @param task
* @param callback
*/
runTask(task, callback) {
if (this.freeWorkers.length === 0) {
this.once(kWorkerFreedEvent, () => this.runTask(task, callback));
return;
}
const worker = this.freeWorkers.pop();
worker[kTaskInfo] = new WorkerPoolTaskInfo(callback);
worker.postMessage(task);
}
/**
* 关闭线程
*/
close() {
for (const worker of this.workers) {
worker.terminate();
}
}
}
module.exports = WorkerPool;
其中task2.js是定义好的一个计算两个数字相加的脚本,内容如下。
const { parentPort } = require('worker_threads');
parentPort.on('message', (task) => {
parentPort.postMessage(task.a + task.b);
});
调用这个线程池非常简单,用例如下所示。
const WorkerPool = require('./worker_pool.js');
const os = require('os');
const pool = new WorkerPool(os.cpus().length);
let finished = 0;
for (let i = 0; i < 10; i++) {
pool.runTask({ a: 42, b: 100 }, (err, result) => {
console.log(i, err, result);
if (++finished === 10)
pool.close();
});
}
锋利的NodeJS之NodeJS多线程的更多相关文章
- 使用node-inspector调试nodejs程序<nodejs>
1.npm install -g node-inspector // -g 导入安装路径到环境变量 一般是c盘下AppData目录下 2.node-inspector & //启动node- ...
- 【nodejs】nodejs 的linux安装(转)
(一) 编译好的文件 简单说就是解压后,在bin文件夹中已经存在node以及npm,如果你进入到对应文件的中执行命令行一点问题都没有,不过不是全局的,所以将这个设置为全局就好了. ./node -v ...
- nodejs01--什么是nodejs,nodejs的基本使用
nodejs使用范围 -直接在cmd命令行运行,在你的电脑上直接运行 -可以搭建一个web服务器(express,koa) -一些基本的使用 -modules是如何工作的 -npm管理modules ...
- 【Nodejs】Nodejsの環境構築
参考URL:http://www.runoob.com/nodejs/nodejs-install-setup.html Windowにインストールする方法を紹介します. ▲ダウンロードURL:htt ...
- [NodeJs] 用Nodejs+Express搭建web,nodejs路由和Ajax传数据并返回状态,nodejs+mysql通过ajax获取数据并写入数据库
小编自学Nodejs,看了好多文章发现都不全,而且好多都是一模一样的 当然了,这只是基础的demo,经供参考,但是相信也会有收获 今天的内容是用Nodejs+Express搭建基本的web,然后呢no ...
- 【NodeJs】Nodejs系列安装
nodejs安装—npm安装—(其他基于这俩项的另写) windows环境 1)nodejs安装 ①下载对应系统版本的Node.js:https://nodejs.org/en/download/ e ...
- nodeJS基础---->nodeJS的使用(一)
Node.js是一个Javascript运行环境(runtime).实际上它是对Google V8引擎进行了封装.V8引擎执行Javascript的速度非常快,性能非常好.Node.js对一些特殊用例 ...
- 使用NodeJS模块-NodeJS官方提供的核心模块
除了使用自己写的本地模块以外,NodeJS可以使用另外两种类型的模块,分别是NodeJS官方提供的核心模块和第三方提供的模块 NodeJS官方提供的核心模块 NodeJS平台自带的一套基本的功能模块, ...
- Meteor + node-imap(nodejs) + mailparser(nodejs) 实现完整收发邮件
版本信息: Meteor:windows MIS安装 0.6.4 node-imap:npm指定的0.8.0版,不是默认的0.7.x版. mailparser:npm安装0.3.6 以下是记录踩到的 ...
随机推荐
- Bootstrap5 多级dropdown
<div class="dropdown"> <a class="btn dropdown-toggle"> Dropdown link ...
- ts 使用 keyof typeof
传递参数 const cats = { "Coding Cat": "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy. ...
- 磁盘使用率/文件大小查看指南du & df
一.前言 磁盘使用率,文件大小查看是我们日常使用命令.这两个是配合使用的,磁盘使用率过高告警了,那么得找到对应的磁盘(df),然后找到对应磁盘下的哪个目录和文件占用了空间(du). df(Disk f ...
- Python_20行代码实现微信消息防撤回(简易版)
学习了一下如何用python实现微信消息的防撤回, 主要思路就是: 时时监控微信,将对方发送的消息缓存下来 如果对方撤回了消息,就将该缓存信息发送给文件传输助手 但其实这功能,基本上毫无意义,看到别人 ...
- 在测试自定义starter时,若出现无法找到helloservice的Bean的解决方法
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoc ...
- CVer想知道的都在这里了,一起分析下《中国计算机视觉人才调研报告》吧!
最近闲来无事,老潘以一名普通算法工程师的角度,结合自身以及周围人的情况,理性也感性地分析一下极市平台前些天发布的2020年度中国计算机视觉人才调研报告. 以下的"计算机视觉人才"简 ...
- java实现压缩文件
原文链接:https://www.cnblogs.com/zeng1994/p/7862288.html
- 如何创建一个Maven项目(eclipse版本)
1 Maven概念 Maven是一个构建项目和管理项目依赖的工具 2 Maven运行原理 这里需要引入两个词汇,叫 本地仓库.中央仓库 本地仓库:就字面意思,存储在自己电脑上的文件夹(需要自己手动创建 ...
- SpringBoot(八):SpringBoot中配置字符编码 Springboot中文乱码处理
SpringBoot中配置字符编码一共有两种方式 方式一: 使用传统的Spring提供的字符编码过滤器(和第二种比较,此方式复杂,由于时间原因这里先不介绍了,后续补上) 方式二(推荐使用) 在appl ...
- 手把手教你手写一个最简单的 Spring Boot Starter
欢迎关注微信公众号:「Java之言」技术文章持续更新,请持续关注...... 第一时间学习最新技术文章 领取最新技术学习资料视频 最新互联网资讯和面试经验 何为 Starter ? 想必大家都使用过 ...