- 后端
- 微服务
- 云原生
【免费下载链接】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. 🌈
导读
@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 的基础上做了两层增强:
- 接入 Midway 的 Service Factory 机制:提供
PiscinaService与PiscinaServiceFactory,统一管理线程池的生命周期; - 支持在 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 线程内解析处理函数的优先级是:
- 指定了
handler时,优先查找 Worker 模块的具名导出; - 未找到时尝试
default导出; - Worker 模块本身是函数(CommonJS 直接导出函数)时直接使用;
- 都找不到则抛出
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 的并发任务数 }, }, };关键参数说明:
| 参数 | 作用 | 说明 |
|---|---|---|
workerFile | Worker 入口文件路径 | 必填,推荐绝对路径,支持带或不带扩展名 |
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解析实例,拥有完整的请求作用域)。
注意事项
- 数据传递:传递给 Worker 的数据会被序列化,不支持函数、类实例等不可序列化对象;
- 线程数配置:根据 CPU 核心数合理配置
maxThreads,避免过多线程导致上下文切换开销; - 内存管理:Worker 线程有独立的内存空间,注意避免内存泄漏;
- 错误处理:Worker 中的错误会被捕获并传递回主线程,需要适当处理(如任务取消时捕获
AbortError,handler 缺失时捕获Handler not found错误); - 路径问题: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. 🌈
相关推荐
gemma-2-2b-it-MT-SimPO模型安装教程:一台普通电脑跑起20亿参数的中英翻译
gemma 2 2b it MT SimPO模型安装教程:一台普通电脑跑起20亿参数的中英翻译 如果你正在为一款"预算有限、又要尽快上线"的翻译功能发愁,这本
三步构建智能茅台预约系统:从零到一的自动化工具部署指南
三步构建智能茅台预约系统:从零到一的自动化工具部署指南 在数字消费时代,效率成为稀缺资源。面对茅台预约这种需要精准时间把控和持续投入的任务,传统人工操作已显乏力
搜索引擎可观测性日志分析链路追踪后端全文检索Spring Framework任务执行器:TaskExecutor线程池配置
Spring Framework任务执行器:TaskExecutor线程池配置 在现代应用开发中,多线程处理是提升系统性能的关键技术之一。Spring Fram
后端Web框架依赖注入
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考