Node黑魔法之无痛用上多线程

正在找工作,有没有大佬带带我的?

brambles:坐标深圳,贵司还缺前端吗?zhuanlan.zhihu.com图标

前言

NodeJS 从 JavaScript 身上延续下来的异步 IO 和异步单线程事件模型是我们开发 IO 密集型应用的利器,无论是读写数据库,还是 HTTP 请求,甚至即时通讯都能得心应手,但是唯一我们绕不过的就是如果有密集的运算怎么办?如果要对外提供拥有需要运算的服务呢?多线程这个坎,恐怕还是绕不过吧。

程序的阻塞

在我们平时给自己写小玩具的时候,尤其是写命令行程序的时候,其实阻塞并没有什么问题。因为这个程序只对我们一个人负责,他跑完了再给告诉我结果完全没问题。但是如果这个程序需要同时服务多个人呢?如果我一个人的请求把程序阻塞了,那么对于其他人来说,这个程序没办法给他们服务,甚至没办法告诉别人它还能不能服务。

一个程序如果会阻塞,那么无非就几种情况:

  1. IO 阻塞。磁盘、网络等等。
  2. 密集运算。程序需要连续且大量的运算。
  3. 写了个死循环。嗯……这个你们自己分锅吧。

所以,NodeJs 帮我们把 IO 的阻塞绕过去了,那现在剩下的一个问题就是,如果需要密集运算的时候怎么办?

Napa.js

Microsoft/napajsgithub.com图标

Napa.js 是微软家的一个 node 项目,用于 Node 的多线程开发。跟其他 Node 的多线程库以及 Node 自带的 Cluster 这种以及很多类似 Web Worker 那样实现的 Node 多线程库不一样,微软爸爸给你提供的是一个解决方案而不是仅仅是多线程环境而已。

比如微软爸爸的 Napa.js 在创建一个 Zone 的时候,自带了一个线程池+任务队列,给外部的借口只需要把需要运行的函数扔进来,就能非常愉快的利用多线程来避免运算密集带来的阻塞,比如给你们贴一段代码出来:

var napa = require('napajs');
var zone1 = napa.zone.create('zone1', { workers: 4 });

// Broadcast code to all 4 workers in 'zone1'.
zone1.broadcast('console.log("hello world");');

// Execute an anonymous function in any worker thread in 'zone1'.
zone1.execute(
    (text) => text, 
    ['hello napa'])
    .then((result) => {
        console.log(result.value);
    });

这样就用多线程异步执行函数了!是不是很厉害呢?但是!这并不是 Napa.js 真正正确的用法!比如在这个问题下面如何评价Microsoft的开源项目napajs?说了 Napa.js 的一些问题,其中包括一些所谓的前端大v。其实这时候前端大v的局限性就体现出来了,不过这篇文章不是专门讲前端大v的,所以略过不表。

Napa.js 的功能是很 OK 的,但是最大的问题就是 API 设计的不尽人意。不过这点当然难不倒我们,造轮子上黑魔法!真正干实事的工具,根本不需要了解那么多概念的。

Napa-Loader

bramblex/napa-loadergithub.com图标napa-loaderwww.npmjs.com

上一节我们说过了,Napa.js 的 API 不尽如人意,那毫无疑问我们给他封装一套易用的 API 不就行了吗?没错的, Napa-Loader 就是这么产生的。虽然只有短短的一百来行代码,但是用了挺多奇技淫巧的,就是为了能够让人用得更舒服。

比如我们有下面这个模块,fib 是求斐波那契额数列的第 n 项的函数。大家写代码的都知道,下面这个求斐波那契额数列的方式是非常耗时的,n 到 40 的时候可能就要小等一会儿了。

// test-module.js

function fib(n) {
    if (n === 0) return 0;
    else if (n === 1) return 1;
    else return fib(n - 1) + fib(n - 2)
}

module.exports = { fib } 

因为 JavaScript 是单线程执行 JavaScript 代码的,所以下面的这样子直接 require 以及直接调用 fib 函数必然会导致整个 node 的程序的卡死。如果我们只是单纯只服务一个对象,那么卡死就卡死嘛,但是如果我们的服务是对外的服务,要响应成千上万人的访问的时候,那么这几秒钟的卡死是完全不可以接受的。

const { fib } = require('./test-module.js')
fib(40)

那么,如何使用我的 Napa-Loader 呢?我的 Napa-Loader 可以自动导入一个 JavaScript 的模块,并且将所有的函数包装成多线程异步执行的函数,比如如下代码那样,就会利用额外的线程进行运算而不会卡死主线程。(注,这里的 await 都是用来注明函数返回是一个 Promise 的,外部的 async function 我就省略掉了)

const { zrequire } = require('napa-loader')
const { fib } = await zrequire('./test-module.js')
await fib(40)

当然功能不止这些,详细的大家请去 Github 围观哈~。

总结

这个 Napa-Loader 还是花了我一点时间的,Node 在运算密集的时候一直都是一个坑。虽然微软爸爸的 Napa.js 看上去还不是那么成熟,但是在好用这一点上,我觉得通过我的 Napa-Loader 加持,已经远超其他实现一大截了。

毕竟,我们只不过想要一个简单方便能够解决问题的工具而已。

编辑于 2019-10-08

文章被以下专栏收录