最近刚好有朋友在问Node.js多线程的问题,我总结了一下,可以考虑使用源码包里面的worker_threads或者第三方的模块来实现。

首先明确一下多线程在Node.js中的概念,然后在聊聊worker_threads的用法。天生异步,真心强大。

  1. 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实例来实现。

官网上给了一个完整的例子,如下所示。

  1. const {
  2. Worker, isMainThread, parentPort, workerData
  3. } = require('worker_threads');
  4. if (isMainThread) {
  5. module.exports = function parseJSAsync(script) {
  6. return new Promise((resolve, reject) => {
  7. const worker = new Worker(__filename, {
  8. workerData: script
  9. });
  10. worker.on('message', message => console.log(message));
  11. worker.on('error', reject);
  12. worker.on('exit', (code) => {
  13. if (code !== 0)
  14. reject(new Error(`Worker stopped with exit code ${code}`));
  15. });
  16. });
  17. };
  18. } else {
  19. const { parse } = require('som-parse-libary');
  20. const script = workerData;
  21. parentPort.postMessage(parse(script));
  22. }

笔者对以上代码开始解析,重点概念如下所示:

Worker该类代表一个独立的js执行线程。

isMainThead一个布尔值,当前代码是否运行在Worker线程中。

parentPortMessagePort对象,如果当前线程是个生成的Worker线程,则允许和父线程通信。

workerData一个可以传递给线程构造函数的任何js数据的的复制数据。

Worker_theads还提供了很多实用的API,整理如下所示。

1.worker.getEnvironmentData(key)

可以获取环境变量,先使用setEnvironmentData来设置环境变量,然后再使用g

etEnvironmentData来获取。

举一个简单的例子,代码如下所示。

  1. const {
  2. Worker,
  3. isMainThread,
  4. setEnvironmentData,
  5. getEnvironmentData,
  6. } = require('worker_threads');
  7. if (isMainThread) {
  8. setEnvironmentData('Hi', 'Node.js!');
  9. const worker = new Worker(__filename);
  10. } else {
  11. console.log(getEnvironmentData('Hi'));.
  12. }
  1. 执行这段代码,可以在控制台打印出“Node.js”字符串。
  1. isMainThread

    isMainThread可以用来判断该进程是不是主线程,如果是主线程,则返回true,否则返回false。下面编写一个嵌套worker的代码,用于展示。
  1. const { Worker, isMainThread } = require('worker_threads');
  2. if (isMainThread) {
  3. console.log("This is a main thread\r\n");
  4. // This re-loads the current file inside a Worker instance.
  5. new Worker(__filename);
  6. } else {
  7. console.log('Inside Worker!');
  8. console.log(isMainThread); // Prints 'false'.
  9. }
  1. MessageChannel和相关用法

    MessageChannel是worker_threads提供的一个双向异步的消息通信信道。下面这段代码就展示了两个MessagePort对象互相传递消息的过程,我们如果想主动结束某个Channel,那么可以使用close事件来完成。
  1. const {MessageChannel} = require('worker_threads');
  2. const {port1, port2} = new MessageChannel();
  3. // port1给port2发送信息
  4. port1.postMessage({carName: 'BYD'});
  5. port2.on('message', (message) => {
  6. console.log("I receive message is ", message);
  7. })
  8. // port2给port1发送信息
  9. port2.postMessage({personality: "Brave"});
  10. port1.on('message', (message) => {
  11. console.log("I receive message is ", message);
  12. });

运行上面的代码,可以在控制台看到如下输出:

  1. I receive message is { personality: 'Brave' }
  2. I receive message is { carName: 'BYD' }

port.on(‘message’)方法是利用被动等待的方式接收事件,如果想手动接收信息可以使用receiveMessageOnPort方法,指定从某个port接收消息,如下所示。

  1. const { MessageChannel, receiveMessageOnPort } = require('worker_threads');
  2. const {port1, port2} = new MessageChannel();
  3. port1.postMessage({Name: "freePHP"});
  4. let result = receiveMessageOnPort(port2);
  5. console.log(result);
  6. let result2 = receiveMessageOnPort(port2);
  7. console.log(result2);

运行上面的代码,可以得到如下输出。

  1. { message: { Name: 'freePHP' } }
  2. undefined

从结果可以看出,receiveMessageOnPort可以指定从另一个MessagePort对象获取消息,是一次消耗消息。

实际工作中,我们不可能只使用单个线程来完成任务,所以需要创建线程池来维护和管理worker thread对象。为了简化线程池的实现,假设只会传递一个woker脚本作为参数,具体实现如下所示。需要单独安装async_hooks模块,它用于异步加载资源。

  1. const { AsyncResource } = require('async_hooks'); // 用于异步加载资源
  2. const { EventEmitter } = require('events');
  3. const path = require('path');
  4. const { Worker } = require('worker_threads');
  5. const kTaskInfo = Symbol('kTaskInfo');
  6. const kWorkerFreedEvent = Symbol('kWorkerFreedEvent');
  7. class WorkerPoolTaskInfo extends AsyncResource {
  8. constructor(callback) {
  9. super('WorkerPoolTaskInfo');
  10. this.callback = callback;
  11. }
  12. done(err, result) {
  13. this.runInAsyncScope(this.callback, null, err, result);
  14. this.emitDestroy(); // 只会被执行一次
  15. }
  16. }
  17. class WorkerPool extends EventEmitter {
  18. constructor(numThreads) {
  19. super();
  20. this.numThreads = numThreads;
  21. this.workers = [];
  22. this.freeWorkers = [];
  23. for (let i = 0; i < numThreads; i++)
  24. this.addNewWorker();
  25. }
  26. /**
  27. * 添加新的线程
  28. */
  29. addNewWorker() {
  30. const worker = new Worker(path.resolve(__dirname, 'task2.js'));
  31. worker.on('message', (result) => {
  32. // 如果成功状态,则将回调传给runTask方法,然后worker移除TaskInfo标记。
  33. worker[kTaskInfo].done(null, result);
  34. worker[kTaskInfo] = null;
  35. //
  36. this.freeWorkers.push(worker);
  37. this.emit(kWorkerFreedEvent);
  38. });
  39. worker.on('error', (err) => {
  40. // 报错后调用回调
  41. if (worker[kTaskInfo])
  42. worker[kTaskInfo].done(err, null);
  43. else
  44. this.emit('error', err);
  45. // 移除一个worker,然后启动一个新的worker来代替当前的worker
  46. this.workers.splice(this.workers.indexOf(worker), 1);
  47. this.addNewWorker();
  48. });
  49. this.workers.push(worker);
  50. this.freeWorkers.push(worker);
  51. this.emit(kWorkerFreedEvent);
  52. }
  53. /**
  54. * 执行任务
  55. * @param task
  56. * @param callback
  57. */
  58. runTask(task, callback) {
  59. if (this.freeWorkers.length === 0) {
  60. this.once(kWorkerFreedEvent, () => this.runTask(task, callback));
  61. return;
  62. }
  63. const worker = this.freeWorkers.pop();
  64. worker[kTaskInfo] = new WorkerPoolTaskInfo(callback);
  65. worker.postMessage(task);
  66. }
  67. /**
  68. * 关闭线程
  69. */
  70. close() {
  71. for (const worker of this.workers) {
  72. worker.terminate();
  73. }
  74. }
  75. }
  76. module.exports = WorkerPool;

其中task2.js是定义好的一个计算两个数字相加的脚本,内容如下。

  1. const { parentPort } = require('worker_threads');
  2. parentPort.on('message', (task) => {
  3. parentPort.postMessage(task.a + task.b);
  4. });

调用这个线程池非常简单,用例如下所示。

  1. const WorkerPool = require('./worker_pool.js');
  2. const os = require('os');
  3. const pool = new WorkerPool(os.cpus().length);
  4. let finished = 0;
  5. for (let i = 0; i < 10; i++) {
  6. pool.runTask({ a: 42, b: 100 }, (err, result) => {
  7. console.log(i, err, result);
  8. if (++finished === 10)
  9. pool.close();
  10. });
  11. }

锋利的NodeJS之NodeJS多线程的更多相关文章

  1. 使用node-inspector调试nodejs程序<nodejs>

    1.npm install -g node-inspector  // -g 导入安装路径到环境变量 一般是c盘下AppData目录下 2.node-inspector & //启动node- ...

  2. 【nodejs】nodejs 的linux安装(转)

    (一) 编译好的文件 简单说就是解压后,在bin文件夹中已经存在node以及npm,如果你进入到对应文件的中执行命令行一点问题都没有,不过不是全局的,所以将这个设置为全局就好了. ./node -v ...

  3. nodejs01--什么是nodejs,nodejs的基本使用

    nodejs使用范围 -直接在cmd命令行运行,在你的电脑上直接运行 -可以搭建一个web服务器(express,koa) -一些基本的使用 -modules是如何工作的 -npm管理modules ...

  4. 【Nodejs】Nodejsの環境構築

    参考URL:http://www.runoob.com/nodejs/nodejs-install-setup.html Windowにインストールする方法を紹介します. ▲ダウンロードURL:htt ...

  5. [NodeJs] 用Nodejs+Express搭建web,nodejs路由和Ajax传数据并返回状态,nodejs+mysql通过ajax获取数据并写入数据库

    小编自学Nodejs,看了好多文章发现都不全,而且好多都是一模一样的 当然了,这只是基础的demo,经供参考,但是相信也会有收获 今天的内容是用Nodejs+Express搭建基本的web,然后呢no ...

  6. 【NodeJs】Nodejs系列安装

    nodejs安装—npm安装—(其他基于这俩项的另写) windows环境 1)nodejs安装 ①下载对应系统版本的Node.js:https://nodejs.org/en/download/ e ...

  7. nodeJS基础---->nodeJS的使用(一)

    Node.js是一个Javascript运行环境(runtime).实际上它是对Google V8引擎进行了封装.V8引擎执行Javascript的速度非常快,性能非常好.Node.js对一些特殊用例 ...

  8. 使用NodeJS模块-NodeJS官方提供的核心模块

    除了使用自己写的本地模块以外,NodeJS可以使用另外两种类型的模块,分别是NodeJS官方提供的核心模块和第三方提供的模块 NodeJS官方提供的核心模块 NodeJS平台自带的一套基本的功能模块, ...

  9. 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 以下是记录踩到的 ...

随机推荐

  1. BGV劝早买内存

    12月3日,BGV全球首发,上线AOFEX交易所(A网),全球区块链爱好者震惊.很多人争相抢挖BGV,希望能够及早获取BGV带来的红利.有趣的是,随着BGV抢挖人数的增多,NGK内存也迎来了暴涨,在1 ...

  2. H5 常见问题汇总及解决方案

    原文链接:http://mp.weixin.qq.com/s/JVUpsz9QHsNV0_7U-3HCMg H5 项目常见问题汇总及解决方案 -- 由钟平勇分享 转自 https://github.c ...

  3. 从微信小程序到鸿蒙js开发【15】——JS调用Java

    鸿蒙入门指南,小白速来!0基础学习路线分享,高效学习方法,重点答疑解惑--->[课程入口] 目录:1.新建一个Service Ability2.完善代码逻辑3.JS端远程调用4.<从微信小 ...

  4. Docker Elasticsearch 集群配置

    一:选用ES原因 公司项目有些mysql的表数据已经超过5百万了,各种业务的查询入库压力已经凸显出来,初步打算将一个月前的数据迁移到ES中,mysql的老数据就物理删除掉. 首先是ES使用起来比较方便 ...

  5. C语言:试探算法解决“八皇后”问题

    #include <stdio.h> #define N 4 int solution[N], j, k, count, sols; int place(int row, int col) ...

  6. JAVA网络编程基本功之Servlet与Servlet容器

    Servlet与Servlet容器关系 Servlet 比较这两个的区别, 就得先搞清楚Servlet 的含义, Servlet (/ˈsərvlit/ ) 翻译成中文就是小型应用程序或者小服务程序, ...

  7. (三)MySQL锁机制 + 事务

    转: (三)MySQL锁机制 + 事务 表锁(偏读) 偏向MyISAM存储引擎.开销小,加锁快,无死锁,锁定粒度大,发生锁冲突的概率最高,并发最低. 查看当前数据库中表的上锁情况,0表示未上锁. sh ...

  8. 剑指 Offer 42. 连续子数组的最大和 + 动态规划

    剑指 Offer 42. 连续子数组的最大和 题目链接 状态定义: 设动态规划列表 \(dp\) ,\(dp[i]\) 代表以元素 \(4nums[i]\) 为结尾的连续子数组最大和. 为何定义最大和 ...

  9. PAT-1144(The Missing Number)set的使用,简单题

    The Missing Number PAT-1144 #include<iostream> #include<cstring> #include<string> ...

  10. [个人总结]pip安装tensorboard太慢

    在执行pip install语句的时候直接指定国内豆瓣的镜像源进行下载: pip install -i https://pypi.douban.com/simple 你想下载的包的名称 例如下载ten ...