├── test ├── testBenchmark │ ├── constants.js │ ├── slowServer.js │ ├── cal.js │ ├── server.js │ └── client.js ├── testTimeout │ ├── timeout.js │ └── index.js ├── test-mjs │ ├── work.mjs │ └── index.js ├── testParams │ ├── event.js │ └── index.js ├── test-es6 │ ├── index.js │ └── work.js ├── testCancel │ ├── cancel.js │ └── index.js ├── testSingleThreadPool │ ├── sync_2.js │ ├── sync_1.js │ └── index.js ├── testEvent │ ├── event.js │ └── index.js ├── testSync │ ├── sync_1.js │ ├── sync_2.js │ └── index.js ├── testPoolExit │ └── index.js └── testStr │ └── index.js ├── src ├── index.js ├── config.js ├── utils.js ├── work.js ├── constants.js ├── worker.js └── threadPool.js ├── package.json ├── LICENSE └── README.md /test/testBenchmark/constants.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | MAX: 10000, 4 | ROUND: 5, 5 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | constants: require('./constants'), 3 | config: require('./config'), 4 | threadPool: require('./threadPool'), 5 | }; -------------------------------------------------------------------------------- /test/testBenchmark/slowServer.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const cal = require('./cal'); 3 | http.createServer(function(req, res) { 4 | cal(); 5 | res.end('OK'); 6 | }).listen(9297); 7 | -------------------------------------------------------------------------------- /test/testTimeout/timeout.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function() { 3 | return new Promise((resolve) => { 4 | setTimeout(() => { 5 | resolve({code: 0}); 6 | },3000) 7 | }) 8 | } -------------------------------------------------------------------------------- /test/test-mjs/work.mjs: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(); 5 | console.log('mjs'); 6 | },3000) 7 | }) 8 | } -------------------------------------------------------------------------------- /test/testParams/event.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = async function() { 3 | return await new Promise((resolve, reject) => { 4 | setTimeout(() => { 5 | resolve('done'); 6 | },3000) 7 | }) 8 | } -------------------------------------------------------------------------------- /test/test-es6/index.js: -------------------------------------------------------------------------------- 1 | const { defaultThreadPool } = require('../../src').threadPool; 2 | const path = require('path'); 3 | function test() { 4 | defaultThreadPool.submit(path.resolve(__dirname, 'work.js')); 5 | } 6 | test() -------------------------------------------------------------------------------- /test/test-mjs/index.js: -------------------------------------------------------------------------------- 1 | const { defaultThreadPool } = require('../../src').threadPool; 2 | const path = require('path'); 3 | function test() { 4 | defaultThreadPool.submit(path.resolve(__dirname, 'work.mjs')); 5 | } 6 | test() -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 最大的线程数 3 | MAX_THREADS: 50, 4 | // 线程池最大任务数 5 | MAX_WORK: Infinity, 6 | // 默认核心线程数 7 | CORE_THREADS: 10, 8 | // 最大空闲时间 9 | MAX_IDLE_TIME: 10 * 60 * 1000, 10 | }; -------------------------------------------------------------------------------- /test/testCancel/cancel.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function() { 3 | return new Promise((resolve) => { 4 | let i = 0; 5 | while(i++ <100000000) { 6 | 7 | } 8 | resolve(); 9 | }) 10 | } -------------------------------------------------------------------------------- /test/test-es6/work.js: -------------------------------------------------------------------------------- 1 | function es6() { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(); 5 | console.log('es6'); 6 | },3000) 7 | }) 8 | } 9 | 10 | exports.default = es6; -------------------------------------------------------------------------------- /test/testSingleThreadPool/sync_2.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = async function() { 3 | return await new Promise((resolve) => { 4 | setTimeout(() => { 5 | console.log(2, ...arguments); 6 | resolve() 7 | },1000) 8 | }) 9 | } -------------------------------------------------------------------------------- /test/testEvent/event.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = async function() { 3 | return await new Promise((resolve, reject) => { 4 | setTimeout(() => { 5 | resolve({type: 'async event'}); 6 | console.log(1) 7 | },3000) 8 | }) 9 | } -------------------------------------------------------------------------------- /test/testSingleThreadPool/sync_1.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = async function() { 3 | return await new Promise((resolve) => { 4 | setTimeout(() => { 5 | console.log(1, ...arguments); 6 | resolve(); 7 | },3000) 8 | }) 9 | } -------------------------------------------------------------------------------- /test/testSync/sync_1.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = async function() { 3 | return await new Promise((resolve) => { 4 | setTimeout(() => { 5 | resolve({type: 'async'}) 6 | console.log(1, ...arguments) 7 | },3000) 8 | }) 9 | } -------------------------------------------------------------------------------- /test/testSync/sync_2.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = async function() { 3 | return await new Promise((resolve) => { 4 | setTimeout(() => { 5 | resolve({type: 'async'}) 6 | console.log(2, ...arguments) 7 | },1000) 8 | }) 9 | } -------------------------------------------------------------------------------- /test/testBenchmark/cal.js: -------------------------------------------------------------------------------- 1 | const { MAX } = require('./constants'); 2 | module.exports = async function() { 3 | let ret = 0; 4 | let i = 0; 5 | while(i++ < MAX) { 6 | ret++; 7 | Buffer.from(String(Math.random())).toString('base64'); 8 | } 9 | return ret; 10 | } -------------------------------------------------------------------------------- /test/testSingleThreadPool/index.js: -------------------------------------------------------------------------------- 1 | const { defaultSingleThreadPool, defaultFixedThreadPool } = require('../../src').threadPool; 2 | const path = require('path'); 3 | defaultFixedThreadPool.submit(path.resolve(__dirname, 'sync_1.js'), {name: 1}); 4 | 5 | defaultSingleThreadPool.submit(path.resolve(__dirname, 'sync_2.js'), {name: 2}); -------------------------------------------------------------------------------- /test/testSync/index.js: -------------------------------------------------------------------------------- 1 | const { defaultThreadPool } = require('../../src').threadPool; 2 | const path = require('path'); 3 | function test() { 4 | defaultThreadPool.submit(path.resolve(__dirname, 'sync_1.js'), {name: 11}); 5 | 6 | defaultThreadPool.submit(path.resolve(__dirname, 'sync_2.js'), {name: 22}); 7 | } 8 | 9 | test() -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const jsFileRegexp = /\.js$/; 2 | 3 | const mjsFileRegexp = /\.mjs$/; 4 | 5 | function isFunction(func) { 6 | return typeof func === 'function'; 7 | } 8 | 9 | function isJSFile(file) { 10 | return jsFileRegexp.test(file); 11 | } 12 | function isMJSFile(file) { 13 | return mjsFileRegexp.test(file); 14 | } 15 | module.exports = { 16 | isFunction, 17 | isJSFile, 18 | isMJSFile, 19 | }; -------------------------------------------------------------------------------- /test/testPoolExit/index.js: -------------------------------------------------------------------------------- 1 | const { defaultThreadPool } = require('../../src').threadPool; 2 | 3 | async function test() { 4 | await defaultThreadPool.submit('function() { while(1) {} }'); 5 | defaultThreadPool.unref(); 6 | //defaultThreadPool.stop(); 7 | // work1.on('done', function() { 8 | // console.log(...arguments); 9 | // defaultThreadPool.unref(); 10 | // }); 11 | } 12 | 13 | test() -------------------------------------------------------------------------------- /test/testTimeout/index.js: -------------------------------------------------------------------------------- 1 | const { defaultThreadPool } = require('../../src').threadPool; 2 | const path = require('path'); 3 | async function test() { 4 | const worker = await defaultThreadPool.submit(path.resolve(__dirname, 'timeout.js')); 5 | worker.setTimeout(1000); 6 | worker.clearTimeout(); 7 | worker.on('done', function() { 8 | console.log(...arguments); 9 | }) 10 | 11 | } 12 | 13 | test() -------------------------------------------------------------------------------- /src/work.js: -------------------------------------------------------------------------------- 1 | // 任务类,一个任务对应一个id 2 | class Work { 3 | constructor({workId, filename, options}) { 4 | // 任务id 5 | this.workId = workId; 6 | // 任务逻辑,字符串或者js文件路径 7 | this.filename = filename; 8 | // 任务返回的结果 9 | this.data = null; 10 | // 任务返回的错误 11 | this.error = null; 12 | // 执行任务时传入的参数,用户定义 13 | this.options = options; 14 | } 15 | } 16 | 17 | exports.Work = Work; -------------------------------------------------------------------------------- /test/testEvent/index.js: -------------------------------------------------------------------------------- 1 | const { defaultThreadPool } = require('../../src').threadPool; 2 | const path = require('path'); 3 | async function test() { 4 | const worker = await defaultThreadPool.submit(path.resolve(__dirname, 'event.js')); 5 | worker.on('done', function() { 6 | console.log(...arguments) 7 | }) 8 | 9 | worker.on('error', function() { 10 | console.log(...arguments) 11 | }) 12 | } 13 | 14 | test() -------------------------------------------------------------------------------- /test/testBenchmark/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const { defaultSyncThreadPool } = require('../../src').threadPool; 3 | const path = require('path'); 4 | 5 | http.createServer(async function(req, res) { 6 | const worker = await defaultSyncThreadPool.submit(path.resolve(__dirname, 'cal.js')); 7 | worker.on('done', function(ret) { 8 | res.end('ok'); 9 | }); 10 | worker.on('error', function() { 11 | console.log(arguments); 12 | }); 13 | }).listen(9297); 14 | 15 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // 丢弃策略 2 | const DISCARD_POLICY = { 3 | // 报错 4 | ABORT: 1, 5 | // 在主线程里执行 6 | CALLER_RUN: 2, 7 | // 丢弃最老的的任务 8 | OLDEST_DISCARD: 3, 9 | // 丢弃 10 | DISCARD: 4, 11 | // 不丢弃 12 | NOT_DISCARD: 5, 13 | }; 14 | // 线程状态 15 | const THREAD_STATE = { 16 | IDLE: 0, 17 | BUSY: 1, 18 | DEAD: 2, 19 | }; 20 | 21 | // 任务状态 22 | const WORK_STATE = { 23 | PENDDING: 0, 24 | RUNNING: 1, 25 | END: 2, 26 | CANCELED: 3, 27 | }; 28 | 29 | module.exports = { 30 | DISCARD_POLICY, 31 | THREAD_STATE, 32 | WORK_STATE, 33 | }; -------------------------------------------------------------------------------- /test/testStr/index.js: -------------------------------------------------------------------------------- 1 | const { defaultThreadPool } = require('../../src').threadPool; 2 | const path = require('path'); 3 | async function test() { 4 | const work1 = await defaultThreadPool.submit('async function({a, b}) { return a + b; }', {a: 1, b: 1}); 5 | work1.on('done', function() { 6 | console.log(...arguments); 7 | }) 8 | const work = await defaultThreadPool.submit(`async function(params) { return await new Promise((resolve) => {console.log(params); setTimeout(() => {resolve(1)}, 3000)}) }`, {name: 22}); 9 | work.on('done', function() { 10 | console.log(...arguments); 11 | }); 12 | } 13 | 14 | test() -------------------------------------------------------------------------------- /test/testCancel/index.js: -------------------------------------------------------------------------------- 1 | const threadPool = require('../../src').threadPool; 2 | const path = require('path'); 3 | 4 | async function test() { 5 | const MyThreadPool = new threadPool.SingleThreadPool({maxWork: 21, discardPolicy: 3}); 6 | MyThreadPool.submit(path.resolve(__dirname, 'cancel.js')); 7 | const worker = await MyThreadPool.submit(path.resolve(__dirname, 'cancel.js')); 8 | // worker.setTimeout(5000); 9 | worker.on('done', function() { 10 | console.log(...arguments); 11 | }); 12 | worker.on('cancel', function() { 13 | console.log('cancel'); 14 | }); 15 | MyThreadPool.submit(path.resolve(__dirname, 'cancel.js')); 16 | } 17 | 18 | test(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-threadpool", 3 | "version": "1.0.1", 4 | "description": "基于nodejs worker_threads的线程池。耗时操作或nodejs没有提供异步模式的api(例如解密、同步的文件api)都可以在线程池中执行,业务代码只需要返回一个Promise或async函数给线程池库,至于业务逻辑做什么操作,其实都可以,比如setTimeout,异步操作,async await等", 5 | "main": "src/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/theanarkh/nodejs-threadpool.git" 15 | }, 16 | "keywords": [ 17 | "worker_threads", 18 | "nodejs-thread-pool" 19 | ], 20 | "author": "gc", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/theanarkh/nodejs-threadpool/issues" 24 | }, 25 | "homepage": "https://github.com/theanarkh/nodejs-threadpool#readme" 26 | } 27 | -------------------------------------------------------------------------------- /test/testParams/index.js: -------------------------------------------------------------------------------- 1 | const threadPool = require('../../src').threadPool; 2 | const path = require('path'); 3 | async function test() { 4 | const fixedThreadPool = new threadPool.FixedThreadPool({ 5 | coreThreads: 1, 6 | maxIdleTime: 10 * 1000, 7 | maxWork: 1, 8 | discardPolicy: 5 9 | }); 10 | // const fixedThreadPool = new threadPool.CPUThreadPool({ 11 | // maxIdleTime: 10 * 1000, 12 | // maxWork: 1, 13 | // discardPolicy: 5 14 | // }); 15 | const worker = await fixedThreadPool.submit(path.resolve(__dirname, 'event.js')); 16 | const worker2 = await fixedThreadPool.submit(path.resolve(__dirname, 'event.js')); 17 | worker.on('done', function() { 18 | console.log(...arguments) 19 | }) 20 | 21 | worker.on('error', function() { 22 | console.log(...arguments) 23 | }); 24 | 25 | worker2.on('done', function() { 26 | console.log(...arguments) 27 | }) 28 | 29 | worker2.on('error', function() { 30 | console.log(...arguments) 31 | }); 32 | } 33 | 34 | test() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 theanarkh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/testBenchmark/client.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const { ROUND }= require('./constants'); 3 | 4 | function round(count) { 5 | return new Promise((reslove) => { 6 | let i = 0; 7 | let resCount = 0; 8 | let cost = 0; 9 | while(i++ < count) { 10 | const start = Date.now(); 11 | http.request({ 12 | host: '127.0.0.1', 13 | port: 9297 14 | }, function(res) { 15 | res.on('data', function() { 16 | // nothing to do,for emit end event 17 | }); 18 | res.on('end', function() { 19 | cost += (Date.now() - start); 20 | resCount++; 21 | if (resCount === count) { 22 | reslove(+(cost / count).toFixed(2)); 23 | } 24 | }); 25 | }).end(); 26 | } 27 | }) 28 | } 29 | async function main() { 30 | let i = 0; 31 | let count = 0; 32 | const data = []; 33 | while(i++ < ROUND) { 34 | count += 20; 35 | const cost = await round(count); 36 | data.push(cost); 37 | } 38 | console.log(data) 39 | process.exit(0); 40 | } 41 | 42 | main(); -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | const { parentPort } = require('worker_threads'); 2 | const vm = require('vm'); 3 | const { isFunction, isJSFile, isMJSFile } = require('./utils'); 4 | 5 | // 监听主线程提交过来的任务 6 | parentPort.on('message', async (work) => { 7 | try { 8 | const { filename, options } = work; 9 | let aFunction; 10 | if (isJSFile(filename)) { 11 | aFunction = require(filename); 12 | if (typeof aFunction.default === 'function') { 13 | aFunction = aFunction.default; 14 | } 15 | } else if (isMJSFile(filename)) { 16 | const { default: entry } = await import(filename); 17 | aFunction = entry; 18 | } else { 19 | aFunction = vm.runInThisContext(`(${filename})`); 20 | } 21 | 22 | if (!isFunction(aFunction)) { 23 | throw new Error('work type error: js file or string'); 24 | } 25 | work.data = await aFunction(options); 26 | parentPort.postMessage({event: 'done', work}); 27 | } catch (error) { 28 | work.error = error.toString(); 29 | console.log(error.toString()) 30 | parentPort.postMessage({event: 'error', work}); 31 | } 32 | }); 33 | 34 | process.on('uncaughtException', (...rest) => { 35 | console.error(...rest); 36 | }); 37 | 38 | process.on('unhandledRejection', (...rest) => { 39 | console.error(...rest); 40 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodejs-threadpool 2 | 3 | ## 请使用 [piscina](https://github.com/piscinajs/piscina). 4 | 5 | 基于nodejs worker_threads的线程池。耗时操作或nodejs没有提供异步模式的api(例如解密、同步的文件api)都可以在线程池中执行,业务代码只需要返回一个Promise或async函数给线程池库,至于业务逻辑做什么操作,其实都可以,比如setTimeout,异步操作,async await等(设计文档https://zhuanlan.zhihu.com/p/266656697)。 6 | 7 | 支持文件和字符串模式,需要导出一个函数。 8 | 9 | 1 提供的线程池类型 10 | ```cpp 11 | // 同步处理任务 12 | class ThreadPool extends ThreadPool { 13 | constructor(options) { 14 | super({...options, sync: true}); 15 | } 16 | } 17 | // cpu型的线程池,线程数和cpu核数一样,不支持动态扩容 18 | class CPUThreadPool extends ThreadPool { 19 | constructor(options) { 20 | super({...options, coreThreads: cores, expansion: false}); 21 | } 22 | } 23 | // 只有一个线程的线程池,不支持动态扩容 24 | class SingleThreadPool extends ThreadPool { 25 | constructor(options) { 26 | super({...options, coreThreads: 1, expansion: false }); 27 | } 28 | } 29 | // 固定线程数的线程池,不支持动态扩容线程数 30 | class FixedThreadPool extends ThreadPool { 31 | constructor(options) { 32 | super({ ...options, expansion: false }); 33 | } 34 | } 35 | 36 | const defaultThreadPool = new ThreadPool(); 37 | const defaultCpuThreadPool = new CPUThreadPool(); 38 | const defaultFixedThreadPool = new FixedThreadPool(); 39 | const defaultSingleThreadPool = new SingleThreadPool(); 40 | module.exports = { 41 | ThreadPool, 42 | CPUThreadPool, 43 | FixedThreadPool, 44 | SingleThreadPool, 45 | defaultThreadPool, 46 | defaultCpuThreadPool, 47 | defaultFixedThreadPool, 48 | defaultSingleThreadPool, 49 | } 50 | ``` 51 | 2 用户可以自定义线程池类型和参数 52 | ```cpp 53 | 1 coreThreads:核心线程数,默认10个 54 | 2 maxThreads:最大线程数,默认50,只在支持动态扩容的情况下,该参数有效,否则该参数等于核心线程数 55 | 3 timeout:任务执行的超时时间,全局配置,可针对单个任务设置 56 | 4 discardPolicy:任务超过阈值时的处理策略,策略如下 57 | // 报错 58 | ABORT: 1, 59 | // 在主线程里执行 60 | CALLER_RUN: 2, 61 | // 丢弃最老的的任务 62 | DISCARD_OLDEST: 3, 63 | // 丢弃 64 | DISCARD: 4, 65 | // 不丢弃 66 | NOT_DISCARD: 5, 67 | 5 preCreate:是否预创建线程池 68 | 6 maxIdleTime:线程空闲多久后自动退出 69 | 7 maxWork:线程池最大任务数 70 | 8 expansion:是否支持动态扩容线程,阈值是最大线程数 71 | ``` 72 | 3 线程池给用户侧返回的是UserWork类的对象 73 | 支持的api 74 | ```cpp 75 | 设置任务的超时时间 76 | setTimeout 77 | // 取消之前的超时时间设置 78 | clearTimeout 79 | // 取消任务的执行 80 | cancel 81 | ``` 82 | UserWork类继承EventEmitter 83 | 支持的事件有 84 | ```cpp 85 | // 任务超时 86 | timeout 87 | // 任务执行完成,执行结果由用户的业务代码决定,在回调里可以拿到 88 | done 89 | // 任务执行出错,具体原因在回调里可以拿到 90 | error 91 | // 任务过载,当前任务被取消 92 | ``` 93 | 4 使用 94 | 例子1 95 | index.js 96 | ```cpp 97 | const { defaultThreadPool } = require('nodejs-thread-pool').threadPool; 98 | const path = require('path'); 99 | async function test() { 100 | const worker = await defaultThreadPool.submit(path.resolve(__dirname, 'event.js')); 101 | worker.on('done', function() { 102 | console.log(...arguments) 103 | }) 104 | 105 | worker.on('error', function() { 106 | console.log(...arguments) 107 | }) 108 | } 109 | test() 110 | ``` 111 | event.js 112 | 113 | ```cpp 114 | module.exports = async function() { 115 | return await new Promise((resolve, reject) => { 116 | setTimeout(() => { 117 | resolve({type: 'async event'}); 118 | console.log(1) 119 | },3000) 120 | }) 121 | } 122 | ``` 123 | 例子2 124 | ``` 125 | const { defaultThreadPool } = require('nodejs-thread-pool').threadPool; 126 | const path = require('path'); 127 | async function test() { 128 | const work1 = await defaultThreadPool.submit('async function({a, b}) { return a + b; }', {a: 1, b: 1}); 129 | work1.on('done', function() { 130 | console.log(...arguments); 131 | }) 132 | const work = await defaultThreadPool.submit(`async function(params) { return await new Promise((resolve) => {console.log(params); setTimeout(() => {resolve(1)}, 3000)}) }`, {name: 22}); 133 | work.on('done', function() { 134 | console.log(...arguments); 135 | }); 136 | } 137 | 138 | test() 139 | ``` 140 | -------------------------------------------------------------------------------- /src/threadPool.js: -------------------------------------------------------------------------------- 1 | const { Worker, threadId } = require('worker_threads'); 2 | const path = require('path'); 3 | const vm = require('vm'); 4 | const { EventEmitter } = require('events'); 5 | const os = require('os'); 6 | const { Work } = require('./work'); 7 | const { DISCARD_POLICY, THREAD_STATE, WORK_STATE } = require('./constants'); 8 | const config = require('./config'); 9 | const cores = os.cpus().length; 10 | const { isFunction, isJSFile, isMJSFile } = require('./utils'); 11 | const workerPath = path.resolve(__dirname, 'worker.js'); 12 | 13 | // 提供给用户侧的接口 14 | class UserWork extends EventEmitter { 15 | constructor({ workId }) { 16 | super(); 17 | // 任务id 18 | this.workId = workId; 19 | // 支持超时取消任务 20 | this.timer = null; 21 | // 任务状态 22 | this.state = WORK_STATE.PENDDING; 23 | } 24 | // 超时后取消任务 25 | setTimeout(timeout) { 26 | this.timer = setTimeout(() => { 27 | this.timer && this.cancel() && this.emit('timeout'); 28 | }, ~~timeout); 29 | } 30 | // 取消之前设置的定时器 31 | clearTimeout() { 32 | clearTimeout(this.timer); 33 | this.timer = null; 34 | } 35 | // 直接取消任务,如果执行完了就不能取消了,this.terminate是动态设置的 36 | cancel() { 37 | if (this.state === WORK_STATE.END || this.state === WORK_STATE.CANCELED) { 38 | return false; 39 | } else { 40 | this.terminate(); 41 | return true; 42 | } 43 | } 44 | // 修改任务状态 45 | setState(state) { 46 | this.state = state; 47 | } 48 | } 49 | 50 | // 管理子线程的数据结构 51 | class Thread { 52 | constructor({ worker }) { 53 | // nodejs的Worker对象,nodejs的worker_threads模块的Worker 54 | this.worker = worker; 55 | this.threadId = worker.threadId; 56 | // 线程状态 57 | this.state = THREAD_STATE.IDLE; 58 | // 上次工作的时间 59 | this.lastWorkTime = Date.now(); 60 | } 61 | // 修改线程状态 62 | setState(state) { 63 | this.state = state; 64 | } 65 | // 修改线程最后工作时间 66 | setLastWorkTime(time) { 67 | this.lastWorkTime = time; 68 | } 69 | 70 | ref() { 71 | this.worker.ref(); 72 | } 73 | unref() { 74 | this.worker.unref(); 75 | } 76 | // TODO add terminate and submit function 77 | } 78 | 79 | // 线程池基类 80 | class ThreadPool { 81 | constructor(options = {}) { 82 | this.options = options; 83 | // 子线程队列 84 | this.workerQueue = []; 85 | // 核心线程数 86 | this.coreThreads = ~~options.coreThreads || config.CORE_THREADS; 87 | // 线程池最大线程数,如果不支持动态扩容则最大线程数等于核心线程数 88 | this.maxThreads = options.expansion !== false ? Math.max(this.coreThreads, config.MAX_THREADS) : this.coreThreads; 89 | // 超过任务队列长度时的处理策略 90 | this.discardPolicy = options.discardPolicy ? options.discardPolicy : DISCARD_POLICY.NOT_DISCARD; 91 | // 是否预创建子线程 92 | this.preCreate = options.preCreate === true; 93 | // 线程最大空闲时间,达到后自动退出 94 | this.maxIdleTime = ~~options.maxIdleTime || config.MAX_IDLE_TIME; 95 | // 是否预创建线程池 96 | this.preCreate && this.preCreateThreads(); 97 | // 保存线程池中任务对应的UserWork 98 | this.workPool = {}; 99 | // 线程池中当前可用的任务id,每次有新任务时自增1 100 | this.workId = 0; 101 | // 线程池中的任务队列 102 | this.queue = []; 103 | // 线程池总任务数 104 | this.totalWork = 0; 105 | // 支持的最大任务数 106 | this.maxWork = ~~options.maxWork || config.MAX_WORK; 107 | // 处理任务的超时时间,全局配置 108 | this.timeout = ~~options.timeout; 109 | this.pollIdle(); 110 | } 111 | // 支持空闲退出 112 | pollIdle() { 113 | const timer = setTimeout(() => { 114 | for (let i = 0; i < this.workerQueue.length; i++) { 115 | const node = this.workerQueue[i]; 116 | if (node.state === THREAD_STATE.IDLE && Date.now() - node.lastWorkTime > this.maxIdleTime) { 117 | node.worker.terminate(); 118 | } 119 | } 120 | this.pollIdle(); 121 | }, 1000); 122 | timer.unref(); 123 | } 124 | // 预创建线程池,数量等于核心线程数 125 | preCreateThreads() { 126 | let { coreThreads } = this; 127 | while(coreThreads--) { 128 | this.newThread(); 129 | } 130 | } 131 | // 创建线程 132 | newThread() { 133 | const worker = new Worker(workerPath); 134 | const thread = new Thread({worker}); 135 | this.workerQueue.push(thread); 136 | const threadId = worker.threadId; 137 | worker.on('exit', () => { 138 | // 找到该线程对应的数据结构,然后删除该线程的数据结构 139 | const position = this.workerQueue.findIndex((thread) => { 140 | return thread.threadId === threadId; 141 | }); 142 | const exitedThreadArray = this.workerQueue.splice(position, 1); 143 | const exitedThread = exitedThreadArray[0] 144 | // 退出时状态是BUSY说明还在处理任务(非正常退出) 145 | this.totalWork -= exitedThread.state === THREAD_STATE.BUSY ? 1 : 0; 146 | }); 147 | // 和子线程通信 148 | worker.on('message', (result) => { 149 | const { 150 | work, 151 | event, 152 | } = result; 153 | const { data, error, workId } = work; 154 | // 通过workId拿到对应的userWork 155 | const userWork = this.workPool[workId]; 156 | // 不存在说明任务被取消了 157 | if (!userWork) { 158 | return; 159 | } 160 | // 修改线程池数据结构 161 | this.endWork(userWork); 162 | 163 | // 修改线程数据结构 164 | thread.setLastWorkTime(Date.now()); 165 | 166 | // 还有任务则通知子线程处理,否则修改子线程状态为空闲 167 | if (this.queue.length) { 168 | // 从任务队列拿到一个任务交给子线程 169 | this.submitWorkToThread(thread, this.queue.shift()); 170 | } else { 171 | thread.setState(THREAD_STATE.IDLE); 172 | } 173 | 174 | switch(event) { 175 | case 'done': 176 | // 通知用户,任务完成 177 | userWork.emit('done', data); 178 | break; 179 | case 'error': 180 | // 通知用户,任务出错 181 | if (EventEmitter.listenerCount(userWork, 'error')) { 182 | userWork.emit('error', error); 183 | } 184 | break; 185 | default: break; 186 | } 187 | }); 188 | worker.on('error', (...rest) => { 189 | console.error(...rest); 190 | }); 191 | return thread; 192 | } 193 | // 选择处理任务的线程 194 | selectThead() { 195 | // 找出空闲的线程,把任务交给他 196 | for (let i = 0; i < this.workerQueue.length; i++) { 197 | if (this.workerQueue[i].state === THREAD_STATE.IDLE) { 198 | return this.workerQueue[i]; 199 | } 200 | } 201 | // 没有空闲的则随机选择一个 202 | return this.workerQueue[~~(Math.random() * this.workerQueue.length)]; 203 | } 204 | // 生成任务id 205 | generateWorkId() { 206 | return ++this.workId % Number.MAX_SAFE_INTEGER; 207 | } 208 | // 给线程池提交一个任务 209 | submit(filename, options = {}) { 210 | return new Promise(async (resolve, reject) => { 211 | let thread; 212 | // 没有线程则创建一个 213 | if (this.workerQueue.length) { 214 | thread = this.selectThead(); 215 | // 该线程还有任务需要处理 216 | if (thread.state === THREAD_STATE.BUSY) { 217 | // 子线程个数还没有达到核心线程数,则新建线程处理 218 | if (this.workerQueue.length < this.coreThreads) { 219 | thread = this.newThread(); 220 | } else if (this.totalWork + 1 > this.maxWork){ 221 | // 总任务数已达到阈值,还没有达到线程数阈值,则创建 222 | if(this.workerQueue.length < this.maxThreads) { 223 | thread = this.newThread(); 224 | } else { 225 | // 处理溢出的任务 226 | switch(this.discardPolicy) { 227 | case DISCARD_POLICY.ABORT: 228 | return reject(new Error('queue overflow')); 229 | case DISCARD_POLICY.CALLER_RUN: 230 | const workId = this.generateWorkId(); 231 | const userWork = new UserWork({workId}); 232 | userWork.setState(WORK_STATE.RUNNING); 233 | userWork.terminate = () => { 234 | userWork.setState(WORK_STATE.CANCELED); 235 | }; 236 | this.timeout && userWork.setTimeout(this.timeout); 237 | resolve(userWork); 238 | try { 239 | let aFunction; 240 | if (isJSFile(filename)) { 241 | aFunction = require(filename); 242 | if (typeof aFunction.default === 'function') { 243 | aFunction = aFunction.default; 244 | } 245 | } else if (isMJSFile(filename)) { 246 | const { default: entry } = await import(filename); 247 | aFunction = entry; 248 | } else { 249 | aFunction = vm.runInThisContext(`(${filename})`); 250 | } 251 | if (!isFunction(aFunction)) { 252 | throw new Error('work type error: js file or string'); 253 | } 254 | const result = await aFunction(options); 255 | // 延迟通知,让用户有机会取消或者注册事件 256 | setImmediate(() => { 257 | if (userWork.state !== WORK_STATE.CANCELED) { 258 | userWork.setState(WORK_STATE.END); 259 | userWork.emit('done', result); 260 | } 261 | }); 262 | } catch (error) { 263 | setImmediate(() => { 264 | if (userWork.state !== WORK_STATE.CANCELED) { 265 | userWork.setState(WORK_STATE.END); 266 | userWork.emit('error', error.toString()); 267 | } 268 | }); 269 | } 270 | return; 271 | case DISCARD_POLICY.OLDEST_DISCARD: 272 | const work = this.queue.shift(); 273 | // maxWork为1时,work会为空 274 | if (work && this.workPool[work.workId]) { 275 | this.cancelWork(this.workPool[work.workId]); 276 | } else { 277 | return reject(new Error('no work can be discarded')); 278 | } 279 | break; 280 | case DISCARD_POLICY.DISCARD: 281 | return reject(new Error('discard')); 282 | case DISCARD_POLICY.NOT_DISCARD: 283 | break; 284 | default: 285 | break; 286 | } 287 | } 288 | } 289 | } 290 | } else { 291 | thread = this.newThread(); 292 | } 293 | // 生成一个任务id 294 | const workId = this.generateWorkId(); 295 | 296 | // 新建一个UserWork 297 | const userWork = new UserWork({workId}); 298 | this.timeout && userWork.setTimeout(this.timeout); 299 | 300 | // 新建一个work 301 | const work = new Work({ workId, filename, options }); 302 | 303 | // 修改线程池数据结构,把UserWork和Work关联起来 304 | this.addWork(userWork); 305 | 306 | // 选中的线程正在处理任务,则先缓存到任务队列 307 | if (thread.state === THREAD_STATE.BUSY) { 308 | this.queue.push(work); 309 | userWork.terminate = () => { 310 | this.cancelWork(userWork); 311 | this.queue = this.queue.filter((node) => { 312 | return node.workId !== work.workId; 313 | }); 314 | } 315 | } else { 316 | this.submitWorkToThread(thread, work); 317 | } 318 | 319 | resolve(userWork); 320 | }) 321 | } 322 | 323 | submitWorkToThread(thread, work) { 324 | const userWork = this.workPool[work.workId]; 325 | userWork.setState(WORK_STATE.RUNNING); 326 | // 否则交给线程处理,并修改状态和记录该线程当前处理的任务id 327 | thread.setState(THREAD_STATE.BUSY); 328 | thread.worker.postMessage(work); 329 | userWork.terminate = () => { 330 | this.cancelWork(userWork); 331 | thread.setState(THREAD_STATE.DEAD); 332 | thread.worker.terminate(); 333 | } 334 | } 335 | 336 | addWork(userWork) { 337 | userWork.setState(WORK_STATE.PENDDING); 338 | this.workPool[userWork.workId] = userWork; 339 | this.totalWork++; 340 | } 341 | 342 | endWork(userWork) { 343 | delete this.workPool[userWork.workId]; 344 | this.totalWork--; 345 | userWork.setState(WORK_STATE.END); 346 | userWork.clearTimeout(); 347 | } 348 | 349 | cancelWork(userWork) { 350 | delete this.workPool[userWork.workId]; 351 | this.totalWork--; 352 | userWork.setState(WORK_STATE.CANCELED); 353 | userWork.emit('cancel'); 354 | } 355 | 356 | traversal(fn) { 357 | this.workerQueue.forEach((worker) => { 358 | fn(worker); 359 | }); 360 | } 361 | ref() { 362 | this.traversal((worker) => { 363 | worker.ref(); 364 | }); 365 | } 366 | 367 | unref() { 368 | this.traversal((worker) => { 369 | worker.unref(); 370 | }); 371 | } 372 | 373 | stop() { 374 | this.traversal((worker) => { 375 | worker.worker.terminate(); 376 | }); 377 | } 378 | } 379 | 380 | class CPUThreadPool extends ThreadPool { 381 | constructor(options) { 382 | super({...options, coreThreads: cores, expansion: false}); 383 | } 384 | } 385 | 386 | class SingleThreadPool extends ThreadPool { 387 | constructor(options) { 388 | super({...options, coreThreads: 1, expansion: false }); 389 | } 390 | } 391 | 392 | class FixedThreadPool extends ThreadPool { 393 | constructor(options) { 394 | super({ ...options, expansion: false }); 395 | } 396 | } 397 | 398 | const defaultThreadPool = new ThreadPool(); 399 | const defaultCpuThreadPool = new CPUThreadPool(); 400 | const defaultFixedThreadPool = new FixedThreadPool(); 401 | const defaultSingleThreadPool = new SingleThreadPool(); 402 | module.exports = { 403 | ThreadPool, 404 | CPUThreadPool, 405 | FixedThreadPool, 406 | SingleThreadPool, 407 | defaultThreadPool, 408 | defaultCpuThreadPool, 409 | defaultFixedThreadPool, 410 | defaultSingleThreadPool, 411 | } 412 | --------------------------------------------------------------------------------