如何解决Nodejs中CPU密集型的任务

开发 前端
nodejs子线程和nodejs主线程共享一个libuv线程池,如果在子线程中使用了libuv的线程池,会和主线程竞争libuv子线程。从而影响主线程的任务执行。如果是纯cpu的计算,则可以这样使用。

[[340904]]

本文转载自微信公众号「编程杂技」,作者theanarkh  。转载本文请联系编程杂技公众号。

一. 方案对比

 

二. 其他的线程池方案

1 Libuv和nginx的线程池:线程数固定,多个线程共享一个任务队列,没有任务时主动挂起,不会主动退出。

2 Java:线程数运行时可以动态增加,支持空闲退出、任务过载多种处理策略,多种类型的线程池。

三. 诉求

1 提交一个js文件处理cpu型任务,这样比较方便。而不是传一个函数,需要经过各种序列化反序列化。

2 一个全局的线程池,可以支持多种类型的任务,类似libuv线程池

3 空闲过久的线程可以主动退出

4 任务过载可以动态扩展线程数

Nodejs线程池的调研:

1 machenjie/node-thread-pool 任务只能是代码字符串,固定线程数,不支持空闲线程主动退出

2 Truth1984/thread_pools 任务只能是代码字符串,没有实现池化,每次创建一个线程,执行完任务退出。

3 bruno303/node-workers-pool 任务只能是代码字符串,不支持空闲退出

4 zebrajaeger/threadpool 不是线程池的概念 5

psastras/node-threadpool 没有实现池化,不支持空闲退出

6 node-worker-threads-pool 周下载量20k左右,star 80。任务只能是代码字符串,不支持空闲线程退出,固定线程数

7 threads 周下载量20k左右,star 1.1k 是对线程模块的封装,没有实现池化能力

8 poolifier 周下载量5000左右,star 59,任务可以是js文件,一个类型的任务新建一个线程池,无法共享线程池

目前的npm包看起来还不太能满足需求。所以决定写一个。

四.线程池的设计需要考虑的问题

1 对于纯cpu型的任务,线程数和cpu核数要相等才能达到最优的性能,否则过多的线程引起的上下文切换反而会导致性能下降。

2 对于io型的任务,更多的线程理论上是会更好,因为可以更早地给硬盘发出命令,磁盘会优化并持续地处理请求。当然,线程数也不是越多越好。线程过多会引起系统负载过高,过多上下文切换也会带来性能的下降。

3 使用方便、简单

整体架构(原图[1])

 

五. 设计思想

1 任务队列的设计

1.1传统的线程池设计 维护一个共享的任务队列,然后多个线程通过加锁互斥的方式访问该队列,取出任务执行。比如libuv,nginx。

1.2 我们的设计 因为我们是通过js使用nodejs线程池的,队列也是使用js数据结构表示的。所以我们无法通过加锁的方式互斥访问共享队列。这就会引起竞态条件。我们使用的方式是,每个子线程维护自己的任务队列,调度中心把任务提交给子线程,子线程自己插入所维护的队列中。

2 线程类型和任务数 把线程分为核心线程和替补线程。分为几个关键的概念:子线程当前的任务数,线程池的总任务数、核心线程数和最大线程数。在总任务数还没有得到阈值时,所有任务都由核心线程处理,达到阈值后,会创建替补线程处理。

3 过载处理策略和选择线程的策略 任务过载时,就会触发过载处理策略。分为报错、在主线程执行任务、继续交给子线程处理、删除最老的任务。选择线程的策略为选择任务数最少的线程。

4 空闲策略 当没有任务可处理的时候,线程池的线程怎么办?

4.1 传统的设计 使用条件变量机制,把线程阻塞在条件变量中,这时候操作系统不会调度该线程执行,所以不会浪费cpu,等到有新任务到来时,主线程会唤醒被阻塞的子线程。不过阻塞的线程依然占据着系统资源,如果一直没有任务,则浪费资源。

4.2 我们的设计 我们在js层无法像底层线程一样使用条件变量,所以我们无法阻塞自己,这就意味着我们会一直在空转、浪费资源。所以我们设计了线程的空闲退出时间,达到这个时间后,线程退出。尽快释放资源。

5 如何设计用户和线程池的通信 用户提交任务后,如果知道任务什么时候执行完?如何拿到执行结果?执行任务的时候,参数如何传进去?

5.1 传统的设计 用户把需要处理的逻辑封装到函数中,然后子线程中阻塞时执行,执行完后,同步拿到结果。

5.2 我们的设计 但是在nodejs中不太一样。Nodejs使用work_thread模块创建的线程,其实是一个和主线程独立的事件循环。所以我们在子线程里执行任务时,其实就相当于在执行一个nodejs的实例,这就意味着我们可以以同步和异步的方式编程我们任务函数代码。那么以异步方式进行处理的任务,我们如何拿到结果?为了解决以上问题,我们使用函数和Promise方案。用户提交的任务具体表现为一个返回Promise的函数,使用函数是因为我们可以在处理任务(执行函数)时,把用户自定义的参数传进去,使用Promise可以等到用户返回的Promise决议时,拿到返回的值,从而返回给用户。

具体实现:用户定义的逻辑test.js

  1. module.exports = function() { 
  2.     return new Promise((resolve, reject) => { 
  3.         setTimeout(() => { 
  4.             resolve({code: 0}); 
  5.         },3000) 
  6.     }) 

子线程逻辑

  1. const result = await require('./test')(options); 

六.成果

线程池支持的参数

  1. 1 coreThreads:核心线程数,默认10个 
  2. 2 maxThreads:最大线程数,默认50,只在支持动态扩容的情况下,该参数有效,否则该参数等于核心线程数 
  3. 3 sync:线程处理任务的模式,同步则串行处理任务,异步则并行处理任务,不同步等待用户代码的执行结果 
  4. 4 discardPolicy:任务超过阈值时的处理策略,策略如下 
  5. 5 preCreate:是否预创建线程池 
  6. 6 maxIdleTime:线程空闲多久后自动退出 
  7. 7 pollIntervalTime:线程隔多久轮询是否有任务需要处理 
  8. 8 maxWork:线程池最大任务数  
  9. 9 expansion:是否支持动态扩容线程,阈值是最大线程数 

支持的线程池类型

  1. // 串行处理任务队列里的任务 
  2. const defaultSyncThreadPool = new SyncThreadPool(); 
  3. // 并行处理任务队列里的任务 
  4. const defaultAsyncThreadPool = new AsyncThreadPool(); 
  5. // 针对cpu密集型任务的线程池,线程数等于cpu核数 
  6. const defaultCpuThreadPool = new CPUThreadPool(); 
  7. // 线程数固定的线程池 
  8. const defaultFixedThreadPool = new FixedThreadPool(); 
  9. // 只有一个线程的线程池,任务在线程池中按序执行 
  10. const defaultSingleThreadPool = new SingleThreadPool(); 

七. 使用方式

方式1

 

nodejs子线程和nodejs主线程共享一个libuv线程池,如果在子线程中使用了libuv的线程池,会和主线程竞争libuv子线程。从而影响主线程的任务执行。如果是纯cpu的计算,则可以这样使用。下面是这种使用方式下,nodejs的架构。

 

方式2

 

在nodejs主进程外开启一个新的进程进行任务的处理,和主进程保持独立,保证稳定性的同时,也不会和主进程竞争libuv的线程。如果在子线程中需要用到libuv线程池,则使用方式2比较好。下面是方式2对应的nodejs架构。

 

八. 具体例子

 

References

[1] 原图: https://www.processon.com/view/link/5f53a187e401fd60bde1bab1

 

[2] github地址: https://github.com/theanarkh/nodejs-threadpool

 

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2011-11-11 09:11:09

云计算HPC集群

2018-03-22 09:16:06

密集型场景SSD

2012-10-23 09:30:18

计算应用虚拟机Java

2012-12-14 11:00:02

VDI桌面虚拟化

2020-09-30 10:56:13

Facebook 开发开源

2023-01-03 19:11:09

CPUI/O速度

2012-10-08 09:45:22

数据密集型处理系统

2022-06-27 14:01:31

LZ4 分析数据密集型压缩算法

2012-12-17 11:20:51

VDI桌面虚拟化

2011-11-14 13:29:22

移动应用开发移动开发移动互联网

2015-05-11 11:37:04

惠普

2010-04-14 15:30:41

Oracle数据库

2013-08-15 15:24:07

戴尔

2011-03-23 14:42:47

CPU过度消耗

2016-05-09 10:08:05

IBMIBM FlashSy

2011-07-25 08:49:32

服务器整合虚拟化

2020-07-08 07:00:00

LinuxCPU应用程序

2012-11-05 15:53:18

高性能计算北龙超云

2012-10-10 13:59:04

惠普虚拟化环境四插槽
点赞
收藏

51CTO技术栈公众号