【HarmonyOS开发小实践】Worker 创建、生命周期与多级 Worker
Worker 创建、生命周期与多级 Worker
TaskPool 用起来简单,但碰到要长时间占据线程、需要保存句柄状态、或者任务超过 3 分钟的场景,就得请出 Worker 了。Worker 给你一个独立的运行环境,自己管生命周期,自己跟主线程消息通信,自由度更高,代价就是写起来更繁琐。这篇文章把 Worker 的运作机制、创建方式、文件路径规则、生命周期管理和多级 Worker 用法都过一遍。
Worker 的运作机制
Worker 子线程拥有独立的 ArkTS Runtime 实例,包括独立的内存空间、消息队列(MessageQueue)、事件轮询机制(EventLoop)、调用栈(CallStack)。和主线程一样是个完整的执行环境,只是没有 UI 能力。
主线程和 Worker 线程通过 postMessage 互相发消息,数据通过序列化传输。每个 Worker 启动都有内存开销(独立的 Runtime 实例),所以系统限制了 Worker 数量上限。
多核 CPU 上多个 Worker 线程可以真正并行执行,这是真并发,不是时间片轮转。
创建 Worker
Worker 线程文件必须放在 {moduleName}/src/main/ets/ 目录层级之下,否则不会被打包到应用里。创建方式有两种:
自动创建(推荐)。 在 DevEco Studio 里右键 {moduleName} 目录下任意位置 > New > Worker,自动生成模板文件和 build-profile.json5 配置,省事。
手动创建。 自己建文件,然后在 build-profile.json5 里配置:
// Stage 模型
"buildOption": {
"sourceOption": {
"workers": [
"./src/main/ets/workers/worker.ets"
]
}
}
// FA 模型
"buildOption": {
"sourceOption": {
"workers": [
"./src/main/ets/MainAbility/workers/worker.ets"
]
}
}
漏配的话 Worker 文件不会被打包,运行时找不到文件会报错。
文件路径规则
构造 Worker 实例时要传入 Worker 线程文件路径(scriptURL)。Stage 模型下有三种写法:
写法一:{moduleName}/ets/{relativePath}
import { worker } from '@kit.ArkTS';
// 文件在 entry/src/main/ets/workers/worker.ets
const worker1: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets');
// 文件在 testworkers/src/main/ets/ThreadFile/workers/worker.ets
const worker2: worker.ThreadWorker = new worker.ThreadWorker('testworkers/ets/ThreadFile/workers/worker.ets');
写法二:@{moduleName}/ets/{relativePath}
import { worker } from '@kit.ArkTS';
// 加载 har 包里的 Worker,文件在 har/src/main/ets/workers/worker.ets
const worker3: worker.ThreadWorker = new worker.ThreadWorker('@har/ets/workers/worker.ets');
写法三:相对路径(仅包内,不支持跨包)
import { worker } from '@kit.ArkTS';
// 当前文件在 har/src/main/ets/components/mainpage/MainPage.ets
// Worker 文件在 har/src/main/ets/workers/worker.ets
const worker4: worker.ThreadWorker = new worker.ThreadWorker('../../workers/worker.ets');
跨包加载规则比较复杂,整理成一张表:
| 加载方\被加载方 | entry | feature | 应用内 hsp | 跨工程 hsp | 源码 har | 三方 har |
|---|---|---|---|---|---|---|
| entry | 写法一、三 | 写法一 | 写法一 | 不支持 | 写法二 | 不支持 |
| feature | 不支持 | 跨包写法一,包内写法一、三 | 写法一 | 不支持 | 写法二 | 不支持 |
| 应用内 hsp | 不支持 | 写法一 | 跨包写法一,包内写法一、三 | 不支持 | 写法二 | 不支持 |
| 跨工程 hsp | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
| 源码 har | 不支持 | 写法一 | 写法一 | 不支持 | 跨包写法二,包内写法二、三 | 不支持 |
| 三方 har | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 仅包内写法三 |
几个注意点:
- 加载 entry、feature、hsp 包的 Worker 不建议用写法三,推荐写法一,不用拼路径。
- 文件路径后缀
.ets/.ts可以省略。 - 跨 HSP/HAR 包要在 oh-package.json5 里配依赖项。
- 开启
useNormalizedOHMUrl或 HAR 包被打包成三方包时,HAR 包里 Worker 只能用相对路径创建。
FA 模型下 scriptURL 是 Worker 文件相对于 {moduleName}/src/main/ets/MainAbility 的路径:
import { worker } from '@kit.ArkTS';
// 文件在 {moduleName}/src/main/ets/MainAbility/workers/worker.ets
const workerFA1 = new worker.ThreadWorker('workers/worker.ets');
// 文件在 {moduleName}/src/main/ets/workers/worker.ets
const workerFA2 = new worker.ThreadWorker('../workers/worker.ets');
生命周期管理
Worker 创建后需要手动管生命周期。创建和销毁开销不小,建议复用而不是频繁创建。空闲 Worker 仍然占资源,不用了主动调 terminate() 或 close() 销毁。
几个关键点:
terminate()/close()是异步退出。注册的onexit()回调执行完线程才真正退出。- Worker 已销毁或正在销毁时调功能接口会抛错。
- 数量上限:内存允许时最多 64 个 Worker,加上 napi_create_ark_runtime 创建的 runtime 总数不超过 80。超限报错
Worker initialization failure, the number of workers exceeds the maximum. - 内存阈值:1.5GB 和设备物理内存 60% 中较小值。所有 Worker + 主线程累积内存超阈值会触发 OOM 崩溃。
基本用法示例
主线程:
import { ErrorEvent, MessageEvents, worker } from '@kit.ArkTS';
@Entry
@Component
struct Index {
build() {
Column() {
Button('start').onClick(() => {
const workerInstance = new worker.ThreadWorker('entry/ets/workers/worker.ets');
// 接收 Worker 发来的消息,在主线程执行
workerInstance.onmessage = (e: MessageEvents) => {
console.info(`onmessage: ${e.data}`);
};
// 捕获 Worker 内全局异常,在主线程执行
workerInstance.onAllErrors = (err: ErrorEvent) => {
console.error(`onAllErrors: ${err.message}`);
};
// 接收到无法序列化的消息时调用
workerInstance.onmessageerror = () => {
console.error('onmessageerror');
};
// Worker 销毁时调用,code=0 正常退出,code=1 异常退出
workerInstance.onexit = (code: number) => {
console.info(`onexit code: ${code}`);
};
// 发消息给 Worker
workerInstance.postMessage('1');
})
}
}
}
Worker 文件 worker.ets:
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
// 收到主线程消息,在 Worker 线程执行
workerPort.onmessage = (e: MessageEvents) => {
console.info('workerPort onmessage: ', e.data);
// 给主线程回消息
workerPort.postMessage('2');
};
workerPort.onmessageerror = () => {
console.error('workerPort onmessageerror');
};
workerPort.onerror = (err: ErrorEvent) => {
console.error('workerPort onerror: ', err.message);
};
主线程和 Worker 线程的回调是对称的:主线程有 onmessage / onAllErrors / onmessageerror / onexit,Worker 线程有 onmessage / onmessageerror / onerror。onAllErrors 只在主线程侧有,能捕获 Worker 线程里 onmessage、timer 回调以及文件执行等流程的全局异常。
多级 Worker
Worker 可以创建子 Worker,形成层级关系。父 Worker 在自己的 onmessage 里 new worker.ThreadWorker(...) 就能创建子 Worker。但生命周期管理要特别小心:销毁父 Worker 前必须先销毁所有子 Worker,否则会有不可预期的结果。
推荐写法
宿主线程:
import { worker, MessageEvents, ErrorEvent } from '@kit.ArkTS';
const parentWorker = new worker.ThreadWorker('entry/ets/workers/ParentWorker.ets');
parentWorker.onmessage = (e: MessageEvents) => {
console.info('宿主线程收到父Worker消息 ' + e.data);
};
parentWorker.onexit = () => {
console.info('父Worker退出');
};
parentWorker.onAllErrors = (err: ErrorEvent) => {
console.error('父Worker报错 ' + err.message);
};
parentWorker.postMessage('宿主线程发送消息给父Worker');
ParentWorker.ets:
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
workerPort.onmessage = (e: MessageEvents) => {
if (e.data === '宿主线程发送消息给父Worker') {
const childWorker = new worker.ThreadWorker('entry/ets/workers/ChildWorker.ets');
childWorker.onmessage = (e: MessageEvents) => {
console.info('父Worker收到子Worker消息 ' + e.data);
if (e.data === '子Worker向父Worker发送信息') {
workerPort.postMessage('父Worker向宿主线程发送信息');
}
};
// 关键:子 Worker 退出后再销毁父 Worker
childWorker.onexit = () => {
console.info('子Worker退出');
workerPort.close();
};
childWorker.onAllErrors = (err: ErrorEvent) => {
console.error('子Worker报错 ' + err.message);
};
childWorker.postMessage('父Worker向子Worker发送信息');
}
};
ChildWorker.ets:
import { MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
workerPort.onmessage = (e: MessageEvents) => {
if (e.data === '父Worker向子Worker发送信息') {
console.info('业务执行结束');
workerPort.postMessage('子Worker向父Worker发送信息');
// 子 Worker 任务完成后主动退出
workerPort.close();
}
};
销毁顺序:子 Worker close() → 触发子 Worker onexit → 在子 Worker onexit 里调父 Worker close() → 触发父 Worker onexit。这样保证父 Worker 销毁时子 Worker 已经不在了。
反例 1:父 Worker 销毁后子 Worker 还在发消息
// ParentWorker.ets —— 错误示范
workerPort.onmessage = (e: MessageEvents) => {
const childWorker = new worker.ThreadWorker('entry/ets/workers/ChildWorker.ets');
childWorker.onmessage = (e: MessageEvents) => {
console.info('父Worker收到子Worker消息 ' + e.data);
};
childWorker.onexit = () => {
// 父 Worker 已经或即将退出,再通过父 Worker 端口发消息会出问题
workerPort.postMessage('父Worker向宿主线程发送信息');
};
childWorker.postMessage('父Worker向子Worker发送信息');
// 创建子 Worker 后立刻销毁父 Worker,子 Worker 还在跑
workerPort.close();
};
// ChildWorker.ets —— 错误示范
workerPort.onmessage = (e: MessageEvents) => {
// 父 Worker 销毁后还往父 Worker 发消息
workerPort.postMessage('子Worker向父Worker发送信息');
setTimeout(() => {
workerPort.postMessage('再发一次'); // 父 Worker 已经没了
}, 1000);
};
父 Worker 已经销毁,子 Worker 发的消息没人接,行为不可预期。
反例 2:父 Worker 发起销毁后再创建子 Worker
// ParentWorker.ets —— 错误示范
workerPort.onmessage = (e: MessageEvents) => {
workerPort.close(); // 先发起销毁
// 父 Worker 正在退出,又创建子 Worker
const childWorker = new worker.ThreadWorker('entry/ets/workers/ChildWorker.ets');
childWorker.postMessage('...'); // 父 Worker 可能已经没了
};
创建子 Worker 前要确保父 Worker 处于存活状态。先 close() 再 new ThreadWorker() 顺序就反了。
实践中要注意的
手动管理生命周期。 Worker 没有 TaskPool 那种自动扩缩容。创建销毁开销大,复用为主。不用了一定要 terminate() 或 close(),不然空闲 Worker 一直占内存。
数量上限 64 个。 加上 napi runtime 总数不超过 80。超了直接报错。任务量大的场景用 TaskPool 更合适。
内存阈值 1.5GB。 实际可用数量根据内存动态调整。所有 Worker + 主线程累积内存超阈值会 OOM 崩溃。监控内存占用,及时销毁不用的 Worker。
只能用线程安全的库。 Worker 线程不能操作 UI,不能用线程不安全的模块。AppStorage 也不支持在 Worker 里用。
16MB 序列化限制。 单次 postMessage 数据量上限 16MB。大数据用 ArrayBuffer 转移或 Sendable。
Worker 文件里禁止 export。 在 Worker 文件里 export 任何内容会导致 jscrash。Worker 文件就是个执行体,不是模块。
应用切后台 Worker 暂停。 应用挂起切到后台后,Worker 线程会暂停运行。回来后恢复。如果任务有时效要求,注意这个行为。
terminate 是异步的。 调完 terminate() 后线程不是立刻退出,要等 onexit 回调执行完。在这期间调 Worker 接口会抛错。需要等销毁完成再做后续操作的话,把逻辑放在 onexit 里。
多级 Worker 销毁顺序。 销毁父 Worker 前先销毁所有子 Worker。推荐在子 Worker 的 onexit 里调父 Worker 的 close(),保证顺序正确。
和 TaskPool 的根本差异是虾米
Worker 给你一个完整的独立 Runtime,自己管生命周期,适合长时任务、需要保存状态、依赖线程上下文的场景。TaskPool 是线程池 + 调度器,自动管理,适合独立短任务。Worker 自由度高,代价是代码量和心智负担;TaskPool 简单,但有 3 分钟和 16MB 的硬限制。
选型其实不复杂:任务超过 3 分钟、需要保存句柄状态、内存敏感,用 Worker;其他场景优先 TaskPool。
更多推荐


所有评论(0)