☰
Midway 线程池组件 @midwayjs/piscina 实战指南:Worker 线程池中的 CPU 密集任务与 Midway 容器化执行
2026/10/8 8:03:32 网站建设 项目流程
  • 后端
  • 微服务
  • 云原生

【免费下载链接】midway

🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载

导读

@midwayjs/piscina是 Midway 框架基于 Piscina 封装的 Worker 线程池组件,用于在主线程之外执行 CPU 密集型计算(如数据处理、加密解密、图像处理),避免阻塞 Node.js 主线程。本指南将完整讲解组件的安装引入、两种任务执行模式(普通 Worker 模式与 Midway 容器模式)、任务取消、多线程池管理、配置项解析以及源码级实现原理,帮助你直接上手并在生产项目中合理选型与调优。

组件概览与适用场景

@midwayjs/piscina(当前仓库版本为 4.2.5,底层依赖piscina@5.3.2,要求 Node.js >= 20,见 package.json)在 Piscina 的基础上做了两层增强:

  1. 接入 Midway 的 Service Factory 机制:提供PiscinaService与PiscinaServiceFactory,统一管理线程池的生命周期;
  2. 支持在 Worker 线程内启动完整的 Midway 应用:通过@PiscinaTask装饰器定义任务类,可在 Worker 中直接使用依赖注入。

相关信息一览:

描述
可用于标准项目✅
可用于 Serverless❌
可用于一体化✅
包含独立主框架❌
包含独立日志❌

适用场景以 CPU 密集型计算、耗时较长的同步操作、需要避免阻塞主线程、需要并行处理大量任务的场景为主;由于依赖原生worker_threads,该组件不适用于 Serverless 运行环境。

安装组件

使用 npm 安装:

$ npm i @midwayjs/piscina@4 --save

或者在package.json中增加如下依赖后,重新安装:

{ "dependencies": { "@midwayjs/piscina": "^4.0.0" } }

引入组件

将组件配置到代码中,作为 Midway 的普通组件导入:

import { Configuration } from '@midwayjs/core'; import * as piscina from '@midwayjs/piscina'; @Configuration({ imports: [ piscina ], // ... }) export class MainConfiguration {}

组件内部定义在 configuration.ts:其namespace为piscina,在onReady阶段通过container.getAsync(PiscinaServiceFactory)完成线程池的初始化,在onStop阶段调用factory.stop()释放所有线程池资源。同时它注册了独立的piscinaWorkerLogger日志,日志文件名为midway-piscina-worker.log,供 Worker 内部使用。

基础用法:普通 Worker 模式

框架基于 Service Factory 机制提供了PiscinaService和PiscinaServiceFactory(对应文档 服务工厂),你可以创建单个或者多个线程池对象来管理线程。

注入PiscinaService并调用run方法执行任务:

import { Inject, Provide } from '@midwayjs/core'; import * as piscina from '@midwayjs/piscina'; @Provide() export class UserService { @Inject() piscinaService: piscina.PiscinaService; async heavyTask() { // 调用 compute 函数 const result1 = await this.piscinaService.run({ handler: 'compute', payload: { value: 10 }, }); console.log(result1); // 20 // 调用 heavyComputation 函数 const result2 = await this.piscinaService.run({ handler: 'heavyComputation', payload: { data: [1, 2, 3, 4, 5] }, }); console.log(result2); // 15 } }

这里的handler对应 Worker 文件中的具名导出函数名。从 worker-bootstrap.js 的源码可以看到,Worker 线程内解析处理函数的优先级是:

  1. 指定了handler时,优先查找 Worker 模块的具名导出;
  2. 未找到时尝试default导出;
  3. Worker 模块本身是函数(CommonJS 直接导出函数)时直接使用;
  4. 都找不到则抛出Handler "xxx" not found错误。

测试用例 index.test.ts 验证了基础行为:对compute.worker.ts(计算value * 2)执行run({ handler: 'compute', payload: { value: 10 } })返回20;且允许不传payload(此时参数为undefined,返回0)。

Worker 文件的导出形态

Worker 文件是一个普通的 Node.js 模块,三种合法导出形态如下:

// 形态一:具名导出(配合 run 的 handler 字段使用) export function compute({ value }) { return value * 2; } // 形态二:default 导出函数 export default function ({ value }) { return value * 2; } // 形态三:CommonJS 直接导出函数 module.exports = function ({ value }) { return value * 2; };

使用 Midway 容器:在 Worker 中启用依赖注入

如果需要在 Worker 中使用 Midway 的依赖注入功能,我们需要在线程中单独再启动一个 Midway 环境。

1. 创建线程中的环境

你可以在主项目中单独创建一个目录用来保存线程代码,必须使用defineConfiguration方法来创建入口。目录结构如下:

➜ base-app git:(feat/support_background_task) ✗ tree . ├── package.json └── src ├── configuration.ts ## 主项目入口 └── worker ## worker 目录 ├── index.ts └── task.ts

在 Worker 目录中创建一个新的 Midway 入口配置:

// src/worker/index.ts import { defineConfiguration } from '@midwayjs/core/functional'; import { CommonJSFileDetector } from '@midwayjs/core'; import * as piscina from '@midwayjs/piscina'; export default defineConfiguration({ namespace: 'worker', detector: new CommonJSFileDetector(), imports: [piscina], // 导入 Piscina });

从 worker-bootstrap.js 源码可以确认其工作方式:当 Worker 模块的default导出是FunctionalConfiguration实例时,bootstrap 会在 Worker 线程内调用initializeGlobalApplicationContext({ baseDir, loggerFactory: loggers })初始化全局应用上下文,再从MidwayFrameworkService中取出主框架(即PiscinaWorkerFramework),由该框架完成@PiscinaTask任务的注册与执行。整个 Worker 的 ApplicationContext 只会初始化一次,后续任务复用同一个容器。

2. 编写任务类

和 Midway 其他组件类似,使用@PiscinaTask装饰器定义任务:

// src/worker/task.ts import { PiscinaTask, IPiscinaTask } from '@midwayjs/piscina'; @PiscinaTask('calculate') export class CalculateTask implements IPiscinaTask { async execute(payload: { a: number; b: number; operation: string }) { if (payload.operation === 'add') { return payload.a + payload.b; } else if (payload.operation === 'multiply') { return payload.a * payload.b; } throw new Error('Unknown operation'); } } @PiscinaTask('square') export class SquareTask implements IPiscinaTask { async execute(payload: { value: number }) { return payload.value * payload.value; } }

@PiscinaTask装饰器的参数为一个字符串,代表对外暴露的 handler 名称。从 decorator.ts 源码看,该装饰器做了四件事:

  • 调用DecoratorManager.saveModule(PISCINA_TASK_KEY, target)注册任务模块(PISCINA_TASK_KEY = 'decorator:piscina_task',见 constants.ts);
  • 通过MetadataManager.defineMetadata保存 handler 名称元数据;
  • 自动附加@Provide()使其可被依赖注入;
  • 自动附加@Scope(ScopeEnum.Request),使每个任务在独立的请求作用域中实例化(见 framework.ts 中ctx.requestContext.getAsync(TaskClass)的调用,保证任务实例具备完整的上下文隔离)。

任务类只需实现execute(payload)方法,接口定义见 interface.ts 中的IPiscinaTask<P, R>。

3. 配置主应用

在主应用配置中指定 Worker 目录:

// src/config/config.default.ts import { join } from 'path'; export default { piscina: { client: { // 指定 worker 入口文件 workerFile: join(__dirname, '../worker/index'), }, }, };

主应用需要忽略 Worker 目录,避免冲突:

// src/configuration.ts import { Configuration } from '@midwayjs/core'; import { CommonJSFileDetector } from '@midwayjs/core'; import * as piscina from '@midwayjs/piscina'; @Configuration({ imports: [piscina], detector: new CommonJSFileDetector({ ignore: ['**/worker/**'], // 忽略 worker 目录 }), }) export class MainConfiguration {}

注意:仓库测试夹具 test/fixtures/base-app/src/configuration.ts 正是采用上述结构,主应用与src/worker目录共存,可对照阅读。

4. 执行容器任务

使用runInContainer方法执行 Worker 容器中的任务:

@Provide() export class UserService { @Inject() piscinaService: piscina.PiscinaService; async heavyTask() { // 执行 calculate 任务 - 乘法 const result1 = await this.piscinaService.runInContainer('calculate', { a: 5, b: 6, operation: 'multiply', }); console.log(result1); // 30 // 执行 calculate 任务 - 加法 const result2 = await this.piscinaService.runInContainer('calculate', { a: 10, b: 20, operation: 'add', }); console.log(result2); // 30 // 执行 square 任务 const result3 = await this.piscinaService.runInContainer('square', { value: 7, }); console.log(result3); // 49 } }

从 manager.ts 中MidwayPiscina.runInContainer的实现可以看到,runInContainer(handler, payload, options)本质上是对run的封装:它向 Worker 发送{ handler: 'defineConfiguration', payload: { handler, data: payload } },Worker 端识别后交由框架的executeTask(handler, data)执行。因此,容器模式与普通模式共用同一个线程池和 Worker 入口文件,区别仅在于任务分发路径不同。

容器模式还继承了 Midway 的链路追踪能力:PiscinaWorkerFramework.executeTask通过MidwayTraceService.runWithEntrySpan为每个任务创建名为piscina ${handler}的入口 Span,并附带midway.protocol: 'piscina'、midway.piscina.handler等属性,可通过配置中的tracing.enable、tracing.meta、tracing.extractor自定义追踪行为(见 framework.ts)。

取消任务

使用AbortController可以取消正在运行的任务:

@Provide() export class UserService { @Inject() piscinaService: piscina.PiscinaService; async cancelableTask() { const abortController = new AbortController(); // 3 秒后取消任务 setTimeout(() => { abortController.abort(); }, 3000); try { const result = await this.piscinaService.run( { handler: 'longRunning', payload: { duration: 10000 }, // 10 秒的任务 }, { signal: abortController.signal, // 传递 AbortSignal } ); } catch (error) { console.error('任务被取消:', error); } } }

signal是 Piscina 原生run选项的一部分,runInContainer同样支持通过第三参数传入(见 manager.ts 中runInContainer的options透传)。

多个 Worker Pool

可以配置多个 Worker Pool,每个 Pool 执行不同的任务:

// src/config/config.default.ts export default { piscina: { clients: { // 计算任务池 compute: { workerFile: join(__dirname, '../worker/compute.worker'), maxThreads: 4, }, // 图像处理任务池 image: { workerFile: join(__dirname, '../worker/image.worker'), maxThreads: 2, }, }, }, };

使用不同的 Pool:

@Provide() export class UserService { @Inject() piscinaServiceFactory: piscina.PiscinaServiceFactory; async useDifferentPools() { // 使用计算池 const computePool = this.piscinaServiceFactory.get('compute'); const result1 = await computePool.run({ handler: 'compute', payload: { value: 10 }, }); // 使用图像处理池 const imagePool = this.piscinaServiceFactory.get('image'); const result2 = await imagePool.run({ handler: 'process', payload: { imagePath: '/path/to/image.jpg' }, }); } }

从 manager.ts 源码看,PiscinaServiceFactory继承了 Midway 的ServiceFactory基类,init()阶段调用initClients(this.config, { concurrent: true })并发创建所有 Pool;每个 Pool 实例是独立的MidwayPiscina,构造时用统一的worker-bootstrap.js作为 Piscina 的filename,通过workerData传递真实 Worker 路径(_fullPath)、主应用目录(_mainAppDir)与开发环境标记(_isDevelopmentEnvironment),并可与用户自定义的workerData合并。此外:

  • createClient对缺少workerFile的 Pool 会抛出client(${name}) 'workerFile' is required错误;
  • 每个 Pool 创建时记录一条[midway:piscina] pool(${name}) created with worker: ...日志;
  • 应用关闭时destroyClient会逐个销毁 Pool,销毁失败只记录错误日志而不中断流程。

配置选项

常用配置

Piscina 有非常丰富的线程配置,Midway 侧通过piscina.client(单 Pool)或piscina.clients(多 Pool)下发,配置类型定义在 interface.ts:PiscinaConfig = ServiceFactoryConfigOption<PiscinaPoolConfig>,其中PiscinaPoolConfig继承 Piscina 构造函数选项(剔除filename字段,因为框架内部已固定为worker-bootstrap.js),并强制要求workerFile字段。

export default { piscina: { client: { workerFile: join(__dirname, '../worker/index'), minThreads: 1, // 最小线程数 maxThreads: 4, // 最大线程数 idleTimeout: 60000, // 空闲超时(毫秒) maxQueue: 'auto', // 最大队列长度 concurrentTasksPerWorker: 1, // 每个 Worker 的并发任务数 }, }, };

关键参数说明:

参数作用说明
workerFileWorker 入口文件路径必填,推荐绝对路径,支持带或不带扩展名
minThreads最小线程数空闲时保留的最小 Worker 数量
maxThreads最大线程数线程池上限,建议依据 CPU 核心数设置
idleTimeout空闲超时(毫秒)线程空闲超过该时间后被回收,释放资源
maxQueue任务队列上限'auto'表示自动计算,也可指定具体数字
concurrentTasksPerWorker单 Worker 并发任务数通常为1,保证任务串行执行
workerData传递给 Worker 的自定义数据框架会与内部_fullPath等字段合并后一并注入

多 Pool 配置

export default { piscina: { clients: { default: { workerFile: join(__dirname, '../worker/default.worker'), }, heavy: { workerFile: join(__dirname, '../worker/heavy.worker'), maxThreads: 8, idleTimeout: 30000, }, }, }, };

当使用clients配置时,PiscinaService默认取default客户端(getDefaultClientName?.() || 'default'),若默认实例不存在会抛出piscina default instance not found.错误(见 manager.ts)。

Worker 文件路径说明

  • 支持.ts和.js文件,框架会自动查找;
  • 建议使用不带扩展名的路径,框架按.js -> .ts -> .mjs -> .cjs顺序查找;
  • 生产环境编译后会自动找到对应的.js文件。
// 推荐:不带扩展名 workerFile: join(__dirname, '../worker/compute.worker') // 也可以:显式指定扩展名 workerFile: join(__dirname, '../worker/compute.worker.js') workerFile: join(__dirname, '../worker/compute.worker.ts')

路径解析逻辑实现在 worker-bootstrap.js 的resolveWorkerFile函数中:如果路径已带扩展名且文件存在则直接返回;否则剥离扩展名后按.js -> .ts -> .mjs -> .cjs依次尝试。若最终为.ts文件,bootstrap 会尝试注册ts-node(开发环境),优先读取 Worker 目录下的tsconfig.json,不存在时以transpileOnly: true的 commonjs 模式运行;生产环境没有ts-node时该步骤被静默忽略,直接 require 编译后的.js文件。开发环境下还会将MIDWAY_LOGGER_WRITEABLE_DIR指向主应用目录,保证 Worker 内日志可写。

API 参考

PiscinaService

PiscinaService是一个单例(@Singleton()),内部持有默认 Pool 的MidwayPiscina实例,并通过delegateTargetAllPrototypeMethod代理了MidwayPiscina的全部原型方法(见 manager.ts),因此类型上它与MidwayPiscina完全一致,可用方法包括:

run(task, options?)

执行普通 Worker 任务。

await piscinaService.run( { handler: 'functionName', // Worker 文件中导出的函数名 payload: { /* 数据 */ }, // 传递给函数的参数 }, { signal: abortController.signal, // 可选:AbortSignal transferList: [], // 可选:可转移对象列表 } );
runInContainer(handler, payload?, options?)

执行 Worker 容器中的@PiscinaTask任务。

await piscinaService.runInContainer( 'taskName', // @PiscinaTask 装饰器的参数 { /* 数据 */ }, // 传递给 execute 方法的参数 { signal: abortController.signal, // 可选:AbortSignal } );

PiscinaServiceFactory

PiscinaServiceFactory用于管理多个线程池:

  • get(name):获取指定名称的MidwayPiscina实例(如factory.get('compute'));
  • 由ServiceFactory基类提供的生命周期管理会在应用启动/关闭时自动创建/销毁全部 Pool。

最佳实践

适用场景

  • CPU 密集型计算(数据处理、加密解密、图像处理);
  • 耗时较长的同步操作;
  • 需要避免阻塞主线程的场景;
  • 需要并行处理大量任务的场景。

选择合适的模式

普通 Worker 模式:

  • 适合简单的纯函数计算;
  • 不需要依赖注入;
  • 性能开销更小(Worker 内不需要启动 Midway 容器,见 worker-bootstrap.js 中函数直调路径)。

Midway 容器模式:

  • 需要使用依赖注入;
  • 需要在 Worker 中使用其他服务;
  • 适合复杂的业务逻辑(容器内的任务通过ctx.requestContext解析实例,拥有完整的请求作用域)。

注意事项

  1. 数据传递:传递给 Worker 的数据会被序列化,不支持函数、类实例等不可序列化对象;
  2. 线程数配置:根据 CPU 核心数合理配置maxThreads,避免过多线程导致上下文切换开销;
  3. 内存管理:Worker 线程有独立的内存空间,注意避免内存泄漏;
  4. 错误处理:Worker 中的错误会被捕获并传递回主线程,需要适当处理(如任务取消时捕获AbortError,handler 缺失时捕获Handler not found错误);
  5. 路径问题:Worker 文件路径建议使用绝对路径(如join(__dirname, '../worker/xxx'))。

常见问题

如何传递大量数据?

对于大型数据(如 ArrayBuffer、Buffer),使用transferList避免数据复制(基于可转移对象实现零拷贝移交):

const buffer = new ArrayBuffer(1024 * 1024); await piscinaService.run( { handler: 'process', payload: buffer }, { transferList: [buffer] } );

handler 未找到怎么办?

普通模式下检查 Worker 文件是否具名导出了与handler同名的函数;容器模式下检查@PiscinaTask('xxx')的字符串参数是否与runInContainer('xxx', ...)的第一个参数一致——框架在 framework.ts 中会抛出Task handler "${handler}" not found. Did you forget to use @PiscinaTask('${handler}') decorator?的明确提示,也可调用framework.getTaskHandlers()获取当前 Worker 容器中所有已注册的 handler 名称进行排查。

更多实现细节去哪里看?

  • 组件核心实现:packages/piscina/src/manager.ts、packages/piscina/src/worker-bootstrap.js
  • 容器框架与任务调度:packages/piscina/src/framework.ts
  • 装饰器与类型定义:packages/piscina/src/decorator.ts、packages/piscina/src/interface.ts
  • 测试用例与夹具:packages/piscina/test/index.test.ts、packages/piscina/test/fixtures/base-app/src/worker/task.ts
  • 后端
  • 微服务
  • 云原生

【免费下载链接】midway

🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载

相关推荐

上一篇:5分钟学会B站视频永久保存:m4s-converter完整使用指南
下一篇:D2DX技术解析:基于DirectX 11的《暗黑破坏神2》现代化渲染引擎

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询