前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >node 线程池技术让文档编译起飞

node 线程池技术让文档编译起飞

作者头像
villainhr
发布2019-08-27 13:59:10
1.6K0
发布2019-08-27 13:59:10
举报
文章被收录于专栏:前端小吉米前端小吉米

最近在维护微信文档这块内容,遇到一个问题,文档数量多起来编译时间会变慢,而且有时候会越来越慢。后面,发现文档的编译一直走的是单线程的,只用到了一个核,顿时感觉有套路可以走了。node 在 v10 过后提出了 worker_threads 模块,它是在一个单独的 node v8 实例进程里面,可以创建多个线程来搞 CPU 任务。

tl;dr

下文主要阐述了一下几点:

  • worker_threads 的基本使用和了解
  • 使用线程池模式,来提高 node 进程的计算速度
  • 用 worker_threads 模块,来优化 vuepress 编译速度
  • workerthreads 模块和 cluster、childprocess 之间的用法和区别

worker_threads 简介

Nodejs 核心执行是基于单线程 + eventloop ,底层是基于 libuv 库,在每次循环中,执行一次完整的 eventloop。所以为了实现 threadworker 的方式,只有脱离于 node 单线程,单独提供 worker_threads 模块来实现。当然,nodejs 还有其他方式实现高性能并发,比如 cluster 和 childprocess,不过,这两者在使用和场景上,与 worker_threads 区别还是挺大的。这里我们后面会了解一下。

worker_threads 的应用主要聚焦在 高 CPU 计算,低 I/O 的场景上,比如像现在比较火热的 AI,挖矿计算,或者朴实点的文件编译上。

Note: worker_threads 是在 10.x 版本提出的,但是在使用时,还需要加上 --experimental-worker flag,不过不想加 flag 的话,把 node 版本切到 11.7 以上就行。

worker_threads 抽象上提供 mainThread 和 worker。其中:

  • mainThread 相当于就是 nodejs 的主线程
  • worker 是单独吊起的 worker 子线程

mainThread 通过 newWorker 去实例化子线程,然后通过 MessageChannel 来和 worker 通信。

这里参考一下官网例子,顺道先解释一下。下面的 demoCode,描述的是一个文件即作为 mainThread,也作为 worker 的执行。

代码语言:javascript
复制
const {
  Worker, isMainThread, parentPort, workerData
} = require('worker_threads');

if (isMainThread) {
// 通过 isMainThread 判断,是否是 worker 还是 mainthread
  module.exports = async function parseJSAsync(script) {
    return new Promise((resolve, reject) => {
        // 通过 __dirname 引用自身文件创建 worker
      const worker = new Worker(__filename, {
        workerData: script
      });
      worker.on('message', resolve);
      worker.on('error', reject);
      worker.on('exit', (code) => {
        if (code !== 0)
          reject(new Error(`Worker stopped with exit code ${code}`));
      });
    });
  };
} else {
  // 执行高 CPU 计算
  const { parse } = require('some-js-parsing-library');
  const script = workerData;
  parentPort.postMessage(parse(script));
}

初始化 worker

官方库提供了 Worker 类,用来进行 Worker 的初始化工作。基本格式为:

代码语言:javascript
复制
new Worker(filename[, options])

这里,可以通过两种方式来写一个 worker 内容,一种是文件、另外一种 eval 代码。

使用文件初始化 worker

现在你已经写好了 worker.js,文件路径为 /abs/to/worker.js。那么,在 mainthread 就可以初始化一个 worker.js。

代码语言:javascript
复制
let worker = new Worker("/abs/to/worker.js")

使用 eval 初始化 worker

使用 eval 执行的话,需要设置一下 new Worker 的 eval 参数,将其手动设置为 true.

代码语言:javascript
复制
new Worker(code,{
    eval:true
})

可以看一下实例代码:

代码语言:javascript
复制
// 设置好处
let code = `
let fib(8);
function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib (n - 2);
}`

// 使用 eval 代码执行
let worekr = new Worker(code,{
    eval:true
})

有时候在进行初始化时,worker 其实还依赖于 mainthread 传入的一些常用变量。nodejs 提供了 workerData 来帮助 coder 完成这件事。

传递给 worker 的初始数据

workerData 的传递,只需要将对应的数据,塞给 new Worker 的初始化 workerData 参数。

代码语言:javascript
复制
new Worker(path,{
    workerData:data
})

需要注意的是,workerData 遵循的是 HTML structured clone algorithm,传递给 worker 时,会 deep-clone 一份,防止 数据的循环引用和保证两个线程之间的数据独立性。也就是说,该 workerData 中的数据只能包含一些基础类型:

  • 不能传函数,保证两个线程的独立性
  • 可以传 Object, Array, Buffer 之类的

更多的,可以参考 https://developer.mozilla.org/en-US/docs/Web/API/WebWorkersAPI/Structuredclonealgorithm

那么,在 worker 中,如何调用 workData 的具体数据呢?

在 worker.js 里面,通过 worker_threads 模块提供的 workerData 来获取。这么说有点抽象,用伪代码模拟下。

代码语言:javascript
复制
// mainthread.js
new Worker("worker.js",{
    workerData:{
        website:"villainhr.com"
    }
})

// worker.js
const {
     workerData
} = require('worker_threads');

// 直接通过 workerData 来获取
let {website} = workerData;

worker 的通信

Worker 的通信主要是 IPC 模式,和 webWorker 一样,也是通过 MessagePort 来互传消息。

Mainthread 向 threadWorker 发消息

主要利用 worker 实例上挂载的 postMessage 方法来实现。

代码语言:javascript
复制
let worker = new Worker("worker.js");
worker.postMessage("欢迎关注 零度的田 公众号")

Worker 上接受 mainthread 传递的消息,利用 worker_threads 模块提供的 parentPort 成员对象来。

代码语言:javascript
复制
const {
    parentPort
} = require('worker_threads');

parentPort.on("message",msg=>{
    console.log(msg); // 欢迎关注 零度的田 公众号
})

这里有个很重要的点需要注意下,如果你通过 parentPort 监听了 message 事件,那么该 worker 是不会自动中断的,除非你手动 terminate 掉它。

threadWorker 向 mainthread 发消息

那么返回来,在 worker 中,怎么给 mainThread 传递消息?还是需要利用 parentPort 对象上,挂载的 postMessage 方法。

代码语言:javascript
复制
// worker.js
const {
    parentPort
} = require('worker_threads');

// 向 mainthread 传递信息
parentPort.postMessage("欢迎关注 零度的田 公众号")

worker_threads 最佳实践

在使用 worker 的过程中,通常是将高 cpu 的计算放在 worker 中运行。根据通信的模式,可以分为两种:

  • 每次接收任务时,单独创建一个原始的 worker 任务,使用完毕后销毁
  • 预先根据 cpu 核数,创建线程池,去执行所有任务

上面两种模式的选取主要是根据业务的模式,不过,一般情况下使用 线程池 会更高效些,因为,重复创建相同的 worker 的话,每次都需要经过一遍 js code 的解码、编译、执行的过程,还是有一定的性能损耗的。

所以,官方推荐是 能用线程池,就不要每次创建 worker。线程池的实现,主要在于 worker_pool 的算法,里面重要功能是需要实现 worker 的调度。

这里推荐一个 worker_pool repo node-worker-threads-pool,这个库在判断 worker 是否 空闲有个取巧的办法,就是当 worker 调用 parentPort.postMessage("xxx") API ,返回结果时,就认为该 worker 已经处于空闲状态了。

为了防止这篇内容过于空洞、浮夸,为了证明 我真的不是在吹水。最近在做微信文档构建的时候,使用到 worker_pool 来进行优化。

vuepress 编译实践

前段时间在维护 微信开放文档, 发现每次统一编译需要时间在 110s ~ 200s 之间,差不多 2~3 min 中,有时候如果编译文件过多的话,可能达 5min 左右。后续,随着文件数量的增加,该编译时间可能会拖慢编译的整个流程。

现在的文档的编译是基于 webpack + vue.renderToString 来做的整体编译。webpack 是前端的一个打包工具库,里面的生态已经很成熟。vue.rednerToString 是用来进行 html 的 prerender 方法,作为静态文档的输出部分。

主要的编译过程主要就在上面两个部分当中,为了分析该过程,选择使用小程序开发部分的文件编译,来作为基准。该部分具有的特性为:

  1. 所有 md 文件有 1454, 量级比较大

在 node 版本 8.6,48 核的机器条件下进行编译,总体耗时为:157s

拆分看:

  1. webpack 的编译耗时为:57s,占比 36%
  2. vue.renderToString 的耗时为:100s,占比 64%

所以,这里的主要问题聚焦于,主要减少 vue.renderToString 的时间,尽量减少 webpack 的编译时间。

vue.renderToString 没有提供任何接口来进行性能优化和提升,只是单纯的作为一个模板拼接函数。所以,只能 node 线程入手,即,通过 node 多线程编程充分利用机器性能加快编译速率。

接下来就是 threads_worker 的重点内容了。

其中,vue.renderToString 有一个任务队列,主要是将所有的 pages,按照路径输出模板。通过 worker 的调度器来实现多线程的 renderToString 方案。

代码语言:javascript
复制
initWorker(){
    // 初始化 workerPool 调度器
     this.pool = new StaticPool({
            size: workerCount,
            task: this.workerPath,
            workerData: this.workerData
        })
}

// 执行 vue.renderToString 的任务队列
async renderPages(pages){
        let jobs = pages.map(async page=>{
            let {html,filePath} = await this.pool.exec(page)
            await fs.outputFile(filePath,html)
        })

        await Promise.all(jobs)
        this.pool.destroy() 
    }

经过多线程的优化,整体的编译时间有挺大的优化。

总体编译耗费时间

优化前:157s 优化后:84s

优化比例为:46.156%

workerthreads vs cluster vs childprocess

说道压榨 CPU 性能的点,nodejs 中,除了使用 worker_threads 之外,还有两个模块也能做到, 一个是 cluster 、一个是 child_process

cluster

cluster 是在一个 master process 中,通过 cluster.fork() 来实例化多个 node v8 实例。可以说 cluster 是多进程的模块,常常用来处理多进程的 node 服务,比如像 pm2。

它的使用方式比较重,每次都需要创建一个进程,并初始化自身的 node 实例,像 event-loop,每个进程都是独立的,所以单个进程发生失败,并不会影响到主进程的稳定性。

具体使用,可以参考 node 文档:https://nodejs.org/dist/latest-v10.x/docs/api/cluster.html#clusterhowit_works

child_process

child_process 模块你可以只理解为,它就是一个进程调起的模块。比如,常常用到的:

  • fork
  • exec
  • spawn

它的执行并不仅仅只限于 nodejs,你用其他语言实现也可以,比如说 python, cpp 二进制文件等。而在 child_process 里面就不存在所谓的通信,父进程通过获得子进程的 stderr、stdout、stdio、stdin 来输出。它进程之间传输数据比较难用,没有所谓的 structure clone 的方式去传递一些对象数据之类的。

worker_threads

workerthreads 和上面两者其实都不同,它并没有脱离当前 v8 的进程实例,而是在其中,创建线程,而这些线程和进程类似,都有自己独立的 OS-level API,并且可以使用绝大多数 node 模块。较上面来说,workertrheads 有以下优势:

  • 单进程,多线程
  • 线程间通信方便,通过 MessageChannel 模式,实现基于事件的跨 线程通信。
  • 可以使用 SharedArrayBuffer,实现多个 worker 共用高效内存
  • 使用简单,在一个 node v8 实例中,共用同一个 event-loop 队列。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端小吉米 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • tl;dr
  • worker_threads 简介
    • 初始化 worker
      • worker 的通信
        • worker_threads 最佳实践
          • vuepress 编译实践
          • workerthreads vs cluster vs childprocess
          相关产品与服务
          云开发 CloudBase
          云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档