
一、问题背景
主线程承担界面渲染、用户交互和生命周期分发。如果把图片批处理、批量数据转换、数据库查询或持续监听直接放在主线程,任务执行期间就可能挤占帧渲染时间,表现为卡顿、掉帧或点击无响应。
把任务放到子线程只是第一步。真正容易出问题的是"结果怎么回来":是任务结束后一次性返回,还是中途持续汇报?任务结束后线程是否可以回收?多个任务是分别处理,还是必须等全部完成?
先给结论:
| 业务特征 | 推荐能力 | 返回方式 | 资源管理重点 |
|---|---|---|---|
| 一次性耗时任务,结束后只要结果 | TaskPool + Task |
Promise |
任务结束后由系统回收空闲线程 |
| 多个独立任务,必须汇总全部结果 | TaskGroup |
Promise<Array<T>> |
任务组只执行一次,统一处理结果 |
| 任务执行中需要同步上报进度 | Task + sendData |
onReceiveData + 最终结果 |
sendData 必须在并发函数执行期调用 |
| 长时间监听、回调或流式任务 | LongTask + emitter |
事件回调 | 结束时取消监听并 terminateTask |
| 一个长期存在、可接收多种指令的线程 | Worker |
postMessage/onmessage |
页面退出或业务结束时 terminate |

二、前置条件
1. 版本与 API 边界
- 示例使用 ArkTS 和
@kit.ArkTS,适用于 API 11 及以上的线程通信案例。 Task.sendData()与Task.onReceiveData()从 API 11 开始支持。taskpool.LongTask和taskpool.terminateTask()从 API 12 开始支持。Worker的基础能力从 API 9 开始支持;API 18 及以上建议注册onAllErrors捕获 Worker 生命周期内的异常。- 代码片段只展示并发模块和页面通信关键路径,工程仍需使用与目标设备匹配的 DevEco Studio、HarmonyOS SDK 和签名配置。
2. 线程边界
ArkTS 的并发实例之间默认是内存隔离的。普通对象跨线程传递时会发生序列化,不能把页面组件、控制器或带有 UI 装饰器的对象直接塞进任务函数。并发函数应放在独立的非 UI 文件中,参数和返回值使用可序列化的数据、ArrayBuffer 或经过验证的 Sendable 对象。
@ComponentV2 页面只负责发起任务、接收结果和更新状态;耗时计算函数不要反向导入页面文件。这样既避免子线程解析 UI 装饰器,也让任务更容易单测和复用。
三、核心内容
1. 一次性任务:用 TaskPool + Task 返回最终结果
适合图片处理、数据转换、一次性查询等"输入明确、执行结束后只返回一次结果"的任务。并发函数需要使用 @Concurrent,并保持在模块顶层,不能依赖页面组件的 this。
typescript
// task-functions.ets:只放纯计算逻辑,不导入 ArkUI 页面模块
import { taskpool } from '@kit.ArkTS';
@Concurrent
export function sumRange(start: number, end: number): number {
let total: number = 0;
for (let value: number = start; value <= end; value++) {
total += value;
}
return total;
}
export async function executeOnce(): Promise<number> {
const task: taskpool.Task = new taskpool.Task(sumRange, 1, 100000);
// execute 的结果回到宿主线程,再由页面提交到 @Local 状态。
return await taskpool.execute(task) as number;
}
这里的 Promise 回调仍然运行在宿主线程。不要因为任务已经放到 TaskPool,就在 then 或 await 后继续做大规模数据加工;结果回传后的整理和 UI 更新同样可能阻塞页面。
2. 多个任务:用 TaskGroup 做统一汇总
当多个任务相互独立,但页面必须等它们全部完成后再展示结果,可以把任务加入 TaskGroup。任务组的返回数组与添加任务的顺序对应,页面只需要处理一次汇总结果。
typescript
import { taskpool } from '@kit.ArkTS';
export async function executeGroup(): Promise<Array<number>> {
const group: taskpool.TaskGroup = new taskpool.TaskGroup();
group.addTask(new taskpool.Task(sumRange, 1, 50000));
group.addTask(new taskpool.Task(sumRange, 50001, 100000));
group.addTask(new taskpool.Task(sumRange, 100001, 150000));
// TaskGroup 只适合一次性汇总,不要加入 LongTask 或监听型任务。
return await taskpool.execute(group) as Array<number>;
}
如果数据量很大,可以先切分输入,再用任务组并发处理。切分粒度不要只看 CPU 核数,还要结合任务本身的计算量、序列化成本和返回结果大小做性能测试。短到几乎没有计算量的任务,放入 TaskPool 反而可能增加调度和通信开销。
3. 任务中途回传:sendData 只用于同步执行期
sendData 适合进度、阶段性计数或分段结果。宿主线程先注册 onReceiveData,并发函数再调用 taskpool.Task.sendData()。
typescript
import { taskpool } from '@kit.ArkTS';
@Concurrent
export function sumWithProgress(totalCount: number): number {
let total: number = 0;
for (let value: number = 1; value <= totalCount; value++) {
total += value;
if (value % 1000 === 0) {
// 必须发生在并发函数的执行期,不能延后到异步回调中。
taskpool.Task.sendData(value);
}
}
return total;
}
export async function executeWithProgress(
onProgress: (value: number) => void
): Promise<number> {
const task: taskpool.Task = new taskpool.Task(sumWithProgress, 10000);
task.onReceiveData(onProgress);
return await taskpool.execute(task) as number;
}
原案例中特别提醒:不要在 Promise.then()、事件回调或其他异步回调里直接调用 SendData()。任务函数可能已经返回,Task 的生命周期也可能已经结束,此时消息不一定能回到宿主线程。需要长期监听或异步回调时,使用下一节的 LongTask + emitter。

4. 长时间监听:LongTask + emitter,结束时主动释放
对 HTTP 监听、传感器监听、持续下载或流式处理,不要把监听注册在普通 Task 中。普通任务返回后,TaskPool 可能回收空闲线程;监听回调稍后再次触发时,就会出现线程已回收、回调失效甚至崩溃的风险。
官方案例推荐把监听放入 LongTask,用 emitter 做宿主线程和任务线程之间的双向通知。停止流程必须成对处理:先发停止事件,再取消事件监听,最后调用 terminateTask。
typescript
import { emitter } from '@kit.BasicServicesKit';
import { taskpool } from '@kit.ArkTS';
const DATA_EVENT_ID: number = 3001;
const STOP_EVENT_ID: number = 3002;
@Concurrent
export async function longTaskBody(): Promise<void> {
let timerId: number = 0;
emitter.on({ eventId: STOP_EVENT_ID }, () => {
clearInterval(timerId);
emitter.off(STOP_EVENT_ID);
});
timerId = setInterval(() => {
// 真实场景可在这里转发传感器、网络或流式数据。
emitter.emit({ eventId: DATA_EVENT_ID });
}, 1000);
}
export function startLongTask(): taskpool.LongTask {
const task: taskpool.LongTask = new taskpool.LongTask(longTaskBody);
emitter.on({ eventId: DATA_EVENT_ID }, () => {
// 宿主线程在这里更新业务状态;页面销毁时必须取消该监听。
});
taskpool.execute(task).catch(() => {
stopLongTask(task);
});
return task;
}
export function stopLongTask(task: taskpool.LongTask): void {
emitter.emit({ eventId: STOP_EVENT_ID });
emitter.off(DATA_EVENT_ID);
taskpool.terminateTask(task);
}
上例用定时器模拟持续数据源,真实业务中应替换成具体监听 API,并在停止事件里调用对应的 off、close 或 release。terminateTask 不是替代业务清理的"强制按钮",监听器、定时器、文件句柄和网络请求仍要由业务代码主动关闭。
5. 多种指令和常驻线程:用 Worker
Worker 更适合一个线程长期存在,并按消息执行多种任务的场景。例如编辑器后台解析、持续文件扫描、下载和解压流水线。宿主线程通过 postMessage 发消息,Worker 通过 workerPort.onmessage 接收并用 postMessage 返回结果。
主线程页面使用 V2 装饰器:
typescript
// WorkerPage.ets
import { worker, ErrorEvent, MessageEvents } from '@kit.ArkTS';
interface WorkerRequest {
type: string;
values: Array<number>;
}
interface WorkerResponse {
type: string;
text: string;
}
@Entry
@ComponentV2
struct WorkerPage {
@Local status: string = '等待任务';
private workerInstance: worker.ThreadWorker | undefined = undefined;
aboutToAppear(): void {
this.workerInstance = new worker.ThreadWorker('entry/ets/workers/Worker.ets');
this.workerInstance.onmessage = (event: MessageEvents): void => {
const response: WorkerResponse = event.data as WorkerResponse;
if (response.type === 'result') {
this.status = response.text;
}
};
this.workerInstance.onAllErrors = (error: ErrorEvent): void => {
this.status = `Worker异常:${error.message}`;
};
}
aboutToDisappear(): void {
this.workerInstance?.terminate();
this.workerInstance = undefined;
}
build(): void {
Column() {
Text(this.status)
.fontSize(18)
Button('提交计算任务')
.margin({ top: 16 })
.onClick(() => {
const request: WorkerRequest = {
type: 'sum',
values: [1, 2, 3, 4, 5]
};
this.workerInstance?.postMessage(request);
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
Worker 文件只处理消息和计算,不导入页面:
typescript
// entry/ets/workers/Worker.ets
import { MessageEvents, worker } from '@kit.ArkTS';
interface WorkerRequest {
type: string;
values: Array<number>;
}
interface WorkerResponse {
type: string;
text: string;
}
const workerPort = worker.workerPort;
workerPort.onmessage = (event: MessageEvents): void => {
const request: WorkerRequest = event.data as WorkerRequest;
if (request.type !== 'sum') {
return;
}
let total: number = 0;
for (let index: number = 0; index < request.values.length; index++) {
total += request.values[index];
}
const response: WorkerResponse = {
type: 'result',
text: `计算结果:${total}`
};
workerPort.postMessage(response);
};
页面退出时调用 terminate(),否则 Worker 仍可能持有消息队列和定时器。API 18 及以上优先使用 onAllErrors;如果项目兼容更低 API,需要根据目标版本改用 onerror,并注意 onerror 捕获范围更窄。

四、代码与验证
1. 建议的工程拆分
text
entry/src/main/ets/
├── pages/
│ └── WorkerPage.ets # @Entry、@ComponentV2、@Local、生命周期
├── tasks/
│ ├── TaskPoolTasks.ets # @Concurrent 纯计算函数
│ └── LongTaskTasks.ets # LongTask 与 emitter 的业务任务
└── workers/
└── Worker.ets # workerPort.onmessage 与消息模型
拆分后有三个直接收益:
- 子线程不需要解析页面文件和 UI 装饰器。
- 任务参数、返回值和消息模型集中定义,序列化边界更清晰。
- 页面销毁逻辑集中在
aboutToDisappear,不容易遗留 Worker 或长时任务。
2. 验证路径
- 先验证一次性任务:确认
executeOnce()结果能回到宿主线程,并且页面状态只在宿主线程更新。 - 再验证进度回传:确认
onReceiveData在最终 Promise 结果之前能收到阶段性消息。 - 验证异常路径:主动传入不可序列化或错误数据,确认 Promise 的
catch能捕获任务执行异常;taskpool.execute()的参数序列化失败则要在调用侧使用外层try...catch。 - 验证页面退出:在任务执行中返回上一页,确认
LongTask已停止、Worker 已terminate,事件监听和定时器已取消。 - 使用 DevEco Profiler 或 HiTrace 对比主线程帧渲染、任务执行、结果回调和 UI 更新耗时。重点检查结果回调是否一次性处理了过大的数组。
五、踩坑与注意事项
1. 不要把"异步"误当成"自动多线程"
async/await 和 Promise 只改变异步组织方式,不会自动把 CPU 密集型代码挪到子线程。需要并行计算时,仍要显式使用 TaskPool 或 Worker。
2. 不要在子线程导入 UI 模块
即使没有直接调用页面组件,只要导入链中解析到 V1/V2 UI 装饰器、页面状态或 ArkUI 组件,就可能导致子线程解析失败。任务文件应遵循最小化导入原则。
3. 不要在异步回调中调用 sendData
sendData 依赖当前 Task。任务函数返回后再从 then、定时器或其他回调发送消息,存在 Task 已结束的生命周期问题。持续回调使用 emitter;持续线程使用 Worker。
4. 不要把 TaskGroup 当作常驻调度器
TaskGroup 适合一次性的并行任务汇总,不能加入 LongTask、周期任务或已经执行过的任务。需要按消息不断接收新指令时,应使用 Worker,或者在宿主线程重新创建新的 Task。
5. 任务结果返回后仍可能卡 UI
子线程只负责把工作移出主线程。结果回到宿主线程后,数组合并、JSON 转换、复杂 forEach 和大量组件刷新仍然发生在主线程。必要时分批提交状态,避免一次性刷新过大的 UI 数据集。
6. 资源释放必须和创建成对
setInterval 对应 clearInterval,事件监听对应 off,HTTP 或数据库句柄对应 close/release,Worker 对应 terminate,LongTask 对应 terminateTask。仅等待 Promise 完成,不等于所有监听和句柄都被释放。
六、结论
线程通信的选择不应从 API 名称开始,而应从任务生命周期开始:
- 只要最终结果:
TaskPool + Task。 - 多个结果一起用:
TaskGroup。 - 执行中同步报进度:
sendData + onReceiveData,但只在并发函数执行期发送。 - 长期监听:
LongTask + emitter,主动管理监听和任务生命周期。 - 长期驻留、消息驱动、多种指令:
Worker,页面退出时terminate。
把 UI、任务函数、Worker 消息模型和释放逻辑分开,线程通信才不会从"主线程卡顿"演变成"线程泄漏、回调失效和状态错乱"。