本文转载自微信公众号《编程杂技》,作者theanarkh。转载本文请联系编程杂技公众号。一、方案对比二、其他线程池方案1libuv和nginx线程池:线程数固定,多个线程共享一个任务队列。当没有任务时,会主动挂断,不会自动退出。2Java:运行时可动态增加线程数,支持空闲退出、任务重载等多种处理策略,支持多种类型的线程池。3.申诉1提交一个js文件来处理CPU任务,比较方便。不是传递一个函数,而是需要经过各种序列化和反序列化。2全局线程池,可以支持多种类型的任务,类似于libuv线程池3空闲时间过长的线程可以主动退出4任务重载可以动态扩展线程数Nodejs线程池研究:1machenjie/node-thread-pool任务只能是码串,线程数固定,不支持空闲线程主动退出2Truth1984/thread_pools任务只能是码串,没有实现池化,每次创建一个线程,任务执行后退出。3bruno303/node-workers-pool任务只能是代码串,不支持idleexit4zebrajaeger/threadpool不是线程池的概念5psastras/node-threadpool没有实现池化,不支持idleexit6node-worker-threads-池周下载量20k左右,star80。任务只能是代码串,不支持空闲线程退出,固定线程数7线程,周下载量20k左右,star1.1k是线程模块的封装,无池化能力8poolifier每周下载量5000左右,star59,任务可以是一个js文件,为一类任务创建一个新的线程池,不能共享线程池。目前的npm包好像不能满足需求。于是决定写一篇。4.线程池设计需要考虑的问题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.2我们的设计但是在nodejs中就不一样了。Nodejs使用work_thread模块创建的线程实际上是一个独立于主线程的事件循环。所以当我们在子线程中执行任务时,其实就相当于执行了一个nodejs实例,也就是说我们可以通过同步和异步的方式来编写我们的任务功能代码。那么我们如何获取异步处理的任务的结果呢?为了解决上述问题,我们使用函数和Promise解决方案。用户提交的任务具体表现为一个返回Promise的函数。使用函数是因为我们可以在处理任务(执行函数)的时候传入自定义参数。使用Promise可以等到用户返回的Promiseresolution。返回值,从而返回给用户。具体实现:自定义逻辑test.jsmodule.exports=function(){returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve({code:0});},3000)})}子线程逻辑constresult=awaitrequire('./test')(options);6、结果线程池支持的参数1coreThreads:核心线程数,默认102maxThreads:最大线程数,默认50,只有支持动态扩展时该参数有效,否则该参数等于线程数核心线程3sync:线程处理任务方式,同步串行处理任务,异步并行处理任务,异步等待用户代码的执行结果4discardPolicy:任务超过阈值时的处理策略,策略如下5preCreate:是否预-创建线程池6maxIdleTime:线程空闲多久后自动退出7pollIntervalTime:线程多久轮询一次是否有任务需要处理8maxWork:线程池最大任务数9expansion:是否支持动态扩展threads,threshold为最大threadNumberofthreadpooltypessupported//任务的串行处理sinthetaskqueueconstdefaultSyncThreadPool=newSyncThreadPool();//并行处理任务队列中的任务constdefaultAsyncThreadPool=newAsyncThreadPool();//用于cpu密集型任务的线程池,线程数等于cpu核数constdefaultCpuThreadPool=newCPUThreadPool();//固定线程数的线程池constdefaultFixedThreadPool=newFixedThreadPool();//只有一个线程的线程池,任务在线程池中顺序执行constdefaultSingleThreadPool=newSingleThreadPool();七。使用方法一nodejs子线程和nodejs主线程共享一个libuv线程池。如果子线程中使用了libuv线程池,它会和主线程竞争libuv子线程。从而影响主线程的任务执行。如果是纯cpu计算,可以这样用。下面是nodejs在这种使用模式下的架构。方法二在nodejs主进程外启动一个新进程处理任务,并保持与主进程的独立性,保证稳定性的同时不与主进程竞争libuv线程。如果需要在子线程中使用libuv线程池,最好使用方法2。下面是方法2对应的nodejs架构。八。具体例子参考文献[1]原图:https://www.processon.com/view/link/5f53a187e401fd60bde1bab1[2]github地址:https://github.com/theanarkh/nodejs-threadpool
