最近写多线程的时候遇到一个烦恼,就是用起来实在太麻烦,不管是WebWorker还是worker_threads库,用起来都实在太麻烦了。而且很多时候IO密集和CPU密集操作很多时候是交织的,有没有一种办法,可以直接在代码中方便的使用多线程呢?
以前我们使用Worker要怎么做?现在我们能怎么做?
之前的做法:
// ### 父进程代码
// 比如请求网络数据,IO操作
const apiData = await request('/api/xxx');
// 为了不阻塞eventloop开启子线程,并拿到符合要求的格式
const goodApiData = await new Promise((resolve, reject) => {
const worker = new Worker('子进程文件名xxx.js', {
workerData: apiData
});
worker.on('message', resolve);
worker.on('error', reject);
});
// ### 子线程代码
// 这里处理data数据,CPU密集操作
doSomething(workerData)
// 再发送回父进程
parentPort.postMessage(data);
代码量这么大,还要写2个文件以上文件,数据发送过去再发送回来头都大了!!!费脑!!!
那有没有更好的方法呢?当然使用ncpu就能做到!
使用ncpu的做法:
import {NCPU} from 'ncpu'
// 比如请求网络数据,IO操作
const apiData = await request('/api/xxx');
// 为了不阻塞eventloop开启子线程,并拿到符合要求的格式
const goodApiData = await NCPU.run((data)=>{
// 这里处理data数据,CPU密集操作
doSomething(data)
return data;
}, [apiData]) //使用数组传参,这有点类似apply
使用ncpu果然爽,一个回调函数就把CPU密集型计算搞定了。
爽是爽,但目前有两点强制限制:
- 回调函数不能共用上下文,因为ncpu是使用函数复制的方式来实现的,不会保留函数上下文,所以要求函数是强无副作用函数。
- 传入参数都是使用 HTML structured clone algorithm方式来进行克隆的,而非原值。
但正是这两点强制限制,使得线程更加安全了。因为但多个线程同时操作原值,会导致内存数据更新速度赶不上线程更新的速到,导致另一个线程读取数据不正确。而且我们要处理数据时,通常只需要将大循环和递归计算放入线程的回调函数中,所以这两点强制,反而不是坏事。
目前ncpu的两个版本
一个是ncpu专门为node.js环境设计,另一个是ncpu-web专门为浏览器环境设计。
同时ncpu需要的最低node.js版本是12,而ncpu-web浏览器要求是谷歌浏览器至少60以上,火狐57以上即可。
在使用的时候要注意这些问题哦!
其实我觉得cpu密集型的任务放到一个js文件会更好,尤其是你的计算逻辑非常复杂的时候,不是三两句代码或者一个函数可以解决的,比如这种情况。或许你可以看看这个解决方案nodejs-threadpool
@theanarkh 不不不,其实大部分业务场景CPU密集都是几句话能解决的,重要的是方便。比如
const apiData = [
{
app_name:"qq",
activation:99.865, //活跃度
cover_rate:95.777 //覆盖率
} //此处省略100000条
]
需求求平均活跃度app平均值,然后对大于平均活跃度的数据利用活跃度*覆盖率/100计算活跃指数,最后将活跃指数进行排行。 看起来需求很复杂,但是数据取好,只需要利用CPU算的部分很少代码,如下:
const goodApiData = await NCPU.run((data)=>{
const avg = data.reduce((total,v)=>total+v.activation)/data.length;
const data1 = data.filter((v)=>v.activation>avg)
const data2 = data1.map(e=>{e.activation_rate = e.activation*cover_rate/100;return e;})
return data2.sort((a,b)=>a.activation_rate-b.activation_rate)
}, [apiData]) //使用数组传参,这有点类似apply
看似复杂需求但CPU计算就这么点,而这样几行就能解决的业务是非常多的,这点计算足以卡线上事件循环1-2秒。
我这里不封装线程池就是觉得我这个东西就适合做基础库,而且好的线程池包括错误的异步链路跟踪,多种分发算法支持,触达分发,所以我认为线程池反倒要根据业务来做,小则一个工厂搞定,要大多机负载分发不可避免。
嗯嗯,可以,不过场景会有点受限。而且用池化技术会更好。
直接用函数计算它不香么
@atian25 小公司哪有什么函数计算,没有资源
@atian25 是挺香的😁
公司的简单业务 已经用上了, 超便宜~
这种 CPU 密集型计算,不用函数计算,而是自己搞线程。
有点像不用云服务,自己去机房搭服务器似的。
费劲又不讨好,人力成本也是成本。
@atian25 其实我觉得多线程依然有他的应用场景,用函数计算,粒度还比较粗,比如我要处理一个文件,丢给一个函数处理。这个函数中,使用多线程会更好。不然就是在一个函数里做拆分,分给另一个函数处理,再合并。
场景肯定会有,但我觉得大部分情况可以丢给函数计算。
@atian25 可以理解为重CPU计算丢给函数,node异步接受吗?像对于导出 Excel 这种也是 CPU 密集的,要肿么办?也可以丢给函数吗?
@KokoTa 其实不是,轻度CPU计算和node异步都可以丢函数。 你可以简单的理解为函数就是一个请求云端就启动一个node来运行,但云端资源可以理解成无限,只要你请求来它就开一个来运行,你不需要管资源问题,云端反正按次扣费。 但重CPU计算不行是因为函数是有超时时间的,超过N秒会自己挂(以前是3到5秒,现在不知道有什么样的变化了),当然重CPU放http请求也不合理。 不过我也是三年前写过类似的文章: https://cnodejs.org/topic/5a3541958230827a182937c3 当时用这个做CPU运算争议挺大的,不过很多人看过后,也使用了这个方法,反馈貌似还行。 当然我现在做ncpu主要是为了解决使用方便问题,而不是争一个最佳解决方案,还是那句话,方案都是根据业务看场景定的。
设置超时时间。默认超时时间为60秒,最长为600秒。超过设置的超时时间,函数将以执行失败结束。
重 CPU 很适合在函数计算的,像语雀的很多导出服务都是用它。