UNPKG

@kazura/node-util

Version:
141 lines (123 loc) 3.91 kB
import os from 'os' import { Worker as _Worker, WorkerOptions as _WorkerOptions } from 'worker_threads' import { v4 as uuidv4 } from 'uuid' class NodeWorker extends _Worker { public readonly WORKER_ID: string = uuidv4() public currentResolve: ((value: any) => void) | null = null public currentReject: ((err: Error) => void) | null = null public constructor(filename: string | URL, options?: _WorkerOptions) { super(filename, options) } } export interface WorkerOptions extends _WorkerOptions { /** * 最大并发数 (默认为 CPU 核心数 - 1) */ max?: number } export class Worker<Args extends any[], Ret = any> { /** * 工作线程池 */ private pool: NodeWorker[] = [] /** * 空闲的线程 */ private idlePool: NodeWorker[] = [] /** * 等待的任务 */ private queue: [(worker: NodeWorker) => void, (err: Error) => void][] = [] public constructor(private filename: string | URL, private options: WorkerOptions = {}) { this.options.max = this.options.max ?? Math.max(1, os.cpus().length - 1) // 默认最大并发数为 CPU 核心数 - 1 if (this.options.max < 1) { throw new Error('options.max must be greater than 0') } this.options.workerData = Object.assign({}, this.options.workerData, { WORKER_ID: uuidv4(), // Worker 标识 }) } public async run(...args: Args): Promise<Ret> { const worker = await this.getAvailableWorker() return new Promise<Ret>((resolve, reject) => { worker.currentResolve = resolve worker.currentReject = reject worker.postMessage(args) }) } /** * 停止所有工作线程 */ public stop() { // 使其不阻止进程退出 this.pool.forEach((w) => w.unref()) // 通知所有等待的任务 this.queue.forEach(([_, reject]) => reject(new Error('Main worker pool stopped before a worker was available.')) ) this.pool = [] this.idlePool = [] this.queue = [] } /** * 获取可用的工作线程 * @returns */ private async getAvailableWorker(): Promise<NodeWorker> { // 如果有空闲的工作线程,直接分配给它 if (this.idlePool.length) { return this.idlePool.shift()! } // 如何可以创建新的工作线程 if (this.pool.length < this.options.max!) { const worker = new NodeWorker(this.filename, this.options) worker.on('message', (res) => { worker.currentResolve && worker.currentResolve(res) worker.currentResolve = null // 重新分配工作线程 this.assignDoneWorker(worker) }) worker.on('error', (err) => { worker.currentReject && worker.currentReject(err) worker.currentReject = null }) worker.on('exit', (code) => { const i = this.pool.indexOf(worker) // 从池中删除 if (i > -1) this.pool.splice(i, 1) // code !== 0 非正常退出 if (code !== 0 && worker.currentReject) { worker.currentReject(new Error(`Worker stopped with non-0 exit code ${code}`)) worker.currentReject = null } }) // 将工作线程添加到池中 this.pool.push(worker) return worker } // 没有可用的 worker,需要等待 let resolve: (worker: NodeWorker) => void let reject: (err: Error) => any const onWorkerAvailablePromise = new Promise<NodeWorker>((r, rj) => { resolve = r reject = rj }) this.queue.push([resolve!, reject!]) return onWorkerAvailablePromise } /** * 分配完成的工作线程 * @param worker * @returns */ private assignDoneWorker(worker: NodeWorker) { // 如果有等待的任务,直接分配给它 if (this.queue.length) { const [resolve] = this.queue.shift()! resolve(worker) return } // 否则加入空闲池 this.idlePool.push(worker) } }