鸿蒙 NEXT 并发深水区:彻底搞懂 ArkTS 的 Actor 内存隔离模型
摘要:本文深入解析鸿蒙NEXT(HarmonyOS 5.x)ArkTS的Actor并发模型,通过对比传统共享内存与Actor模型的差异,阐述其通过内存隔离实现无锁并发的优势。重点介绍TaskPool(任务池)和Worker两种并发方案,TaskPool适合短时任务,支持优先级调度与任务取消;Worker适用于长时任务。详细分析跨线程数据传递的四种方式(序列化拷贝、Transfer转移、Shared
适合读者:有一定移动端开发经验(Android / iOS / 前端),想深入理解 HarmonyOS NEXT 并发机制的开发者。
一、从一个痛点出发
做过 Android 开发的同学一定遇过这个经典噩梦:
// 多线程竞争同一个 List,轻轻松松触发 ConcurrentModificationException
new Thread(() -> list.add(item)).start();
new Thread(() -> list.remove(0)).start();
锁(Lock)、同步块(synchronized)、volatile……为了保护共享内存,我们堆了一层又一层的防护。代码越写越脆,Bug 越来越难复现。
HarmonyOS NEXT 在设计 ArkTS 并发体系时,选择了一条不同的路:不让你碰共享内存。
这背后的理论支撑叫做 Actor 并发模型,而它的工程落地,就是 ArkTS 里的 TaskPool 和 Worker。
二、两种并发世界观
先来理解两种主流并发模型的根本区别。
2.1 内存共享模型(Shared Memory Concurrency)
这是 Java/C++ 的传统方式:多个线程共享同一块堆内存,通过"抢锁"来决定谁有权访问。
Thread A ─────────────────┐
├─── 竞争访问 ──► 共享内存 (Heap)
Thread B ─────────────────┘
↑ 谁先拿到锁,谁先操作
问题:死锁、竞态条件(Race Condition)、内存可见性问题……这些都是程序员的梦魇。即便是经验丰富的工程师也经常在这里翻车。
2.2 Actor 模型(Message-Passing Concurrency)
Actor 模型的核心理念只有一句话:每个线程是一个独立的 Actor,有自己私有的内存;Actor 之间只能通过消息通信,不能直接访问彼此的内存。
Actor A (主线程) Actor B (子线程)
┌─────────────────┐ ┌─────────────────┐
│ 私有 VM 实例 │ ──消息/序列化──► │ 私有 VM 实例 │
│ 私有堆内存 │ ◄──消息/序列化── │ 私有堆内存 │
└─────────────────┘ └─────────────────┘
↑ 互不干扰,零竞争
没有共享,就没有竞争。没有竞争,就不需要锁。这是 ArkTS 并发设计的第一性原理。
三、ArkTS 并发的两张牌:TaskPool vs Worker
HarmonyOS NEXT 基于 Actor 模型,提供了两个多线程并发 API:TaskPool(任务池)和 Worker(工作线程)。它们是同一套模型的不同封装层次。
3.1 快速对比
| 维度 | TaskPool | Worker |
|---|---|---|
| 抽象层次 | 任务(Task)维度 | 线程维度 |
| 生命周期 | 系统自动管理 | 开发者手动管理 |
| 适用任务时长 | 短时任务(< 3分钟)¹ | 长时/常驻任务 |
| 线程数量 | 系统自动扩缩容 | 最多 64 个 |
| 优先级调度 | ✅ 支持 | ❌ 不支持 |
| 任务取消 | ✅ 支持 | ❌ 不支持 |
| 推荐场景 | 绝大多数场景 | 需要持久化线程句柄的场景 |
一句话总结:优先用 TaskPool,只有当你需要管理线程生命周期或任务超长时,才用 Worker。
¹ 超过 3 分钟的任务会被系统自动回收,但可以用
new taskpool.LongTask(fn, ...args)替代taskpool.Task来声明长时任务,突破这个限制,同时仍保留 TaskPool 的调度优势。
四、TaskPool 实战拆解
4.1 最简用法
import { taskpool } from '@kit.ArkTS';
// ⚠️ 关键点:必须用 @Concurrent 装饰器标记,且只能用导入变量和局部变量
@Concurrent
function heavyCompute(n: number): number {
let result = 0;
for (let i = 0; i < n; i++) {
result += Math.sqrt(i);
}
return result;
}
@Entry
@Component
struct Index {
@State result: string = '未计算';
async runTask() {
const task = new taskpool.Task(heavyCompute, 10_000_000);
// execute 返回 Promise,await 即可拿到结果
const res = await taskpool.execute(task) as number;
this.result = `计算结果: ${res.toFixed(2)}`;
}
build() {
Column({ space: 20 }) {
Text(this.result).fontSize(18)
Button('开始计算').onClick(() => this.runTask())
}
.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
主线程完全不阻塞,UI 依旧丝滑。这就是 TaskPool 的价值。
4.2 任务优先级与任务组
import { taskpool } from '@kit.ArkTS';
@Concurrent
function processImage(imageId: string): string {
// 模拟图片处理耗时
return `processed_${imageId}`;
}
async function batchProcess(imageIds: string[]) {
const taskGroup = new taskpool.TaskGroup();
for (const id of imageIds) {
const task = new taskpool.Task(processImage, id);
taskGroup.addTask(task);
}
// 等待所有任务完成,类似 Promise.all
const results = await taskpool.execute(taskGroup) as string[];
console.log('全部处理完成:', results);
}
// 高优先级的紧急任务
async function urgentTask(id: string) {
const task = new taskpool.Task(processImage, id);
// 设置为最高优先级,系统会优先调度
await taskpool.execute(task, taskpool.Priority.HIGH);
}
4.3 可取消任务(图库场景)
这是 TaskPool 相比 Worker 独有的能力,非常适合图片预加载这类"滑动时需要频繁取消旧任务"的场景:
import { taskpool } from '@kit.ArkTS';
@Concurrent
function loadImage(url: string): ArrayBuffer {
// 模拟网络请求
return new ArrayBuffer(1024);
}
class ImagePreloader {
private taskMap: Map<string, taskpool.Task> = new Map();
preload(url: string) {
const task = new taskpool.Task(loadImage, url);
this.taskMap.set(url, task);
taskpool.execute(task).then(data => {
console.log(`${url} 加载完成`);
}).catch(err => {
// 被取消时会走到这里
console.log(`${url} 任务已取消`);
});
}
cancel(url: string) {
const task = this.taskMap.get(url);
if (task) {
// ⚠️ 重要限制:cancel 只对还在队列中等待的任务有效
// 如果任务已被线程取走、正在执行,cancel 调用是无效的
taskpool.cancel(task);
this.taskMap.delete(url);
}
}
}
五、Worker 实战拆解
当你需要一个"长期存活"的线程(比如维持一个 WebSocket 连接,或需要持久化的数据库句柄),就要用 Worker。
5.1 Worker 线程文件(workers/MyWorker.ets)
import { workerPort, MessageEvents } from '@ohos.worker';
// Worker 线程的入口,监听主线程消息
workerPort.onmessage = (event: MessageEvents) => {
const { type, payload } = event.data as { type: string; payload: unknown };
if (type === 'COMPUTE') {
const n = payload as number;
let result = 0;
for (let i = 0; i < n; i++) result += i;
// 将结果发回主线程(自动序列化)
workerPort.postMessage({ type: 'RESULT', payload: result });
}
};
workerPort.onmessageerror = (event: MessageEvents) => {
console.error('Worker 消息错误', event);
};
5.2 主线程使用 Worker
import worker from '@ohos.worker';
@Entry
@Component
struct WorkerDemo {
private myWorker: worker.ThreadWorker | null = null;
@State output: string = '等待结果...';
aboutToAppear() {
// 创建 Worker,路径为 Worker 文件的相对路径
this.myWorker = new worker.ThreadWorker('entry/ets/workers/MyWorker');
// 监听 Worker 返回的消息
this.myWorker.onmessage = (event: worker.MessageEvents) => {
const { payload } = event.data as { type: string; payload: number };
this.output = `Worker 计算结果: ${payload}`;
};
}
aboutToDisappear() {
// ⚠️ 必须手动销毁,否则线程泄漏!
this.myWorker?.terminate();
}
build() {
Column({ space: 20 }) {
Text(this.output).fontSize(18)
Button('发送任务给 Worker').onClick(() => {
this.myWorker?.postMessage({ type: 'COMPUTE', payload: 1_000_000 });
})
}
.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
六、最难的部分:跨线程数据怎么传?
这是很多开发者遇坑最多的地方。由于 Actor 模型内存隔离,数据跨线程传递需要"搬家"而非"共享地址"。ArkTS 支持四种方式,各有适用场景。
方式一:普通对象——序列化拷贝(最常用)
import { taskpool } from '@kit.ArkTS';
@Concurrent
function processData(data: Record<string, string>): string {
// data 是从主线程深拷贝过来的,修改它不影响主线程
return data.name.toUpperCase();
}
const obj = { name: 'harmony', version: '5.0' };
taskpool.execute(new taskpool.Task(processData, obj));
// 注意:obj 传入后是拷贝,子线程修改 obj 对主线程无影响
优点:安全简单。缺点:大对象拷贝有性能开销。
方式二:ArrayBuffer——可转移(Transfer)
import { taskpool } from '@kit.ArkTS';
@Concurrent
function encryptBuffer(buf: ArrayBuffer): ArrayBuffer {
// 处理二进制数据
const view = new Uint8Array(buf);
for (let i = 0; i < view.length; i++) {
view[i] ^= 0xFF; // 简单异或加密
}
return buf;
}
async function runEncrypt() {
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
const task = new taskpool.Task(encryptBuffer, buffer);
// transfer 模式:所有权转移给子线程,主线程原始 buffer 变为空
// 避免了 1MB 数据的内存拷贝!
task.setTransferList([buffer]);
const result = await taskpool.execute(task) as ArrayBuffer;
console.log('加密完成,结果长度:', result.byteLength);
}
适用:大文件、图片、音视频等二进制场景。所有权转移,零拷贝。
方式三:SharedArrayBuffer——共享内存
import { taskpool } from '@kit.ArkTS';
// 罕见但有用:多线程同时写入同一块内存
const sharedBuf = new SharedArrayBuffer(4);
@Concurrent
function increment(buf: SharedArrayBuffer) {
const arr = new Int32Array(buf);
// 必须用 Atomics 保证操作原子性,防止数据竞争
Atomics.add(arr, 0, 1);
}
async function runIncrement() {
// 多个 TaskPool 任务共享同一块内存
const tasks: Promise<Object>[] = [];
for (let i = 0; i < 10; i++) {
tasks.push(taskpool.execute(new taskpool.Task(increment, sharedBuf)));
}
await Promise.all(tasks);
console.log('最终计数:', new Int32Array(sharedBuf)[0]); // 应输出 10
}
警告:这是 Actor 模型的"后门",使用时必须配合 Atomics,否则数据竞争问题重现。非必要不使用。
方式四:Sendable 对象——引用共享(ArkTS 独有创新)
这是 HarmonyOS NEXT 相比传统 JS 引擎最大的创新之一。
传统 JS 引擎中,跨线程只能拷贝或转移,没有办法让多个线程引用同一个 JS 对象。但 Sendable 打破了这个限制:
import { taskpool } from '@kit.ArkTS';
import { lang } from '@arkts.lang';
// 用 @Sendable 装饰的类,其实例可以在线程间共享引用
@Sendable
class SharedConfig implements lang.ISendable {
public readonly apiUrl: string;
public readonly timeout: number;
constructor(url: string, timeout: number) {
this.apiUrl = url;
this.timeout = timeout;
}
}
@Concurrent
function fetchData(config: SharedConfig): string {
// config 是跨线程的引用,不是拷贝
// ⚠️ Sendable 对象的属性必须是只读或 Sendable 类型,防止并发写入
return `请求 ${config.apiUrl},超时 ${config.timeout}ms`;
}
// 多个任务共享同一个配置对象,无需序列化拷贝
async function runFetch() {
const config = new SharedConfig('https://api.example.com', 5000);
const tasks: Promise<Object>[] = [];
for (let i = 0; i < 5; i++) {
tasks.push(taskpool.execute(new taskpool.Task(fetchData, config)));
}
const results = await Promise.all(tasks) as string[];
console.log(results);
}
Sendable 的意义在于:对于那些大型只读配置、模型数据,不再需要每次都深拷贝一份给子线程,内存效率大幅提升。
七、完整选型决策树
需要多线程并发?
│
├─ 任务是否需要长期占用线程(> 3分钟)或维持线程状态(数据库连接等)?
│ ├─ 是 ──► 使用 Worker
│ └─ 否 ──► 使用 TaskPool(推荐)
│
传递的数据是什么类型?
│
├─ 普通 JS 对象,数据量小 ──► 默认序列化拷贝
├─ ArrayBuffer / 大二进制数据 ──► Transfer 转移所有权
├─ 多线程都需要写入的共享计数器 ──► SharedArrayBuffer + Atomics
└─ 大型只读对象(配置、模型)──► @Sendable 引用传递
八、几个常见的坑
坑 1:@Concurrent 函数里用了外部变量
import { taskpool } from '@kit.ArkTS';
const multiplier = 3; // 外部闭包变量
// ❌ 编译报错!ArkTS 编译器会拒绝这种写法
@Concurrent
function badFunc(n: number): number {
return n * multiplier; // 不能访问外部变量
}
// ✅ 正确:通过参数传入
@Concurrent
function goodFunc(n: number, m: number): number {
return n * m;
}
taskpool.execute(new taskpool.Task(goodFunc, 5, multiplier));
原因:@Concurrent 函数会被序列化发到子线程执行,子线程没有主线程的闭包上下文,编译期强制检查。
坑 2:Worker 忘记 terminate()
Worker 线程不会自动销毁。页面销毁(aboutToDisappear)时,必须手动调用实例的 terminate() 方法(即 myWorker.terminate()),否则会造成线程泄漏,严重时导致应用崩溃。
坑 3:把大对象扔进 TaskPool 期待高性能
如果你有一个 10MB 的 JSON 对象要传给子线程,序列化拷贝的开销可能比在主线程直接计算还慢。这时候应该考虑 Transfer(如果是 ArrayBuffer)或 @Sendable(如果是只读配置)。
九、横向对比:这套设计有多独特?
| Android(Java/Kotlin) | Web(JS Worker) | ArkTS | |
|---|---|---|---|
| 默认并发模型 | 内存共享 + 锁 | Actor(拷贝) | Actor(拷贝) |
| 跨线程对象共享 | 直接共享(有风险) | ❌ 不支持 | ✅ Sendable 引用传递 |
| 系统托管线程池 | ExecutorService | ❌ | ✅ TaskPool |
| 任务取消 | Future.cancel() | ❌ | ✅ taskpool.cancel() |
| 优先级调度 | Thread.setPriority() | ❌ | ✅ taskpool.Priority |
可以看到,ArkTS 在继承 JS/TS 生态的同时,在并发能力上做了相当大的增强——尤其是 Sendable 对象和 TaskPool 的系统级调度,是传统 Web Worker 所不具备的。
十、总结
ArkTS 的 Actor 并发模型,本质上是用"强制内存隔离"换来了"开发者不需要考虑锁"。这是一笔划算的买卖——大部分业务场景里,你并不需要跨线程共享可变状态;而那些真正需要共享的场景,Sendable 和 SharedArrayBuffer 提供了足够精细的控制。
理解这套模型之后,鸿蒙 NEXT 开发的几个原则自然就清晰了:
- 主线程只做 UI,所有耗时操作丢给
TaskPool - 优先用 TaskPool,需要持久线程才用 Worker
- 数据传递按体量选择:小对象拷贝、大二进制转移、只读大对象 Sendable
@Concurrent函数不能有闭包,这是编译器替你保驾护航
鸿蒙生态正在快速演进,但这套并发设计的基础架构已经相当稳固。搞清楚它,是写出高性能鸿蒙原生应用的第一步。
更多推荐



所有评论(0)