ArkTS 的多线程模型(taskpool、Worker)不像 JVM 那样共享堆内存。跨线程默认走结构化克隆(structured clone),代价是拷贝开销 + 引用断裂:主线程改了一份,子线程看不到。业务里但凡涉及大列表、缓存、状态机在多线程之间流动,克隆语义就成了性能与一致性的双重瓶颈。
Sendable 数据模型的定位就是让一份对象真正跨线程共享,而不是每次传递都深拷贝。它是 ArkTS 静态类型系统 + 运行时并发原语共同支撑出来的机制,本质上把"可跨线程"变成了一种编译期能校验的对象契约。
Sendable 对象在运行时被分配到一个跨线程可访问的堆区域(Shared Heap)。它的字段类型受限,只能是:
collections.Array、collections.Map、collections.Set 等关键约束有三条:
ArkTSUtils.locks.AsyncLock 或 CAS 语义保证。这三条决定了 Sendable 是"零拷贝但有代价"的模型:省下克隆开销,换来对并发正确性的显式管理。
// shared_counter.ets
import { taskpool } from '@kit.ArkTS';
import { collections } from '@kit.ArkTS';
import { ArkTSUtils } from '@kit.ArkTS';
@Sendable
export class SharedCounter {
private value: number = 0;
private lock: ArkTSUtils.locks.AsyncLock = new ArkTSUtils.locks.AsyncLock();
async increment(): Promise<number> {
return this.lock.lockAsync(() => {
this.value += 1;
return this.value;
});
}
get(): number {
return this.value;
}
}
@Concurrent
async function bump(counter: SharedCounter, times: number): Promise<number> {
let last = 0;
for (let i = 0; i < times; i++) {
last = await counter.increment();
}
return last;
}
export async function runDemo(): Promise<number> {
const counter = new SharedCounter();
const tasks: Promise<number>[] = [];
for (let i = 0; i < 4; i++) {
tasks.push(taskpool.execute(bump, counter, 1000) as Promise<number>);
}
await Promise.all(tasks);
return counter.get(); // 期望 4000
}这里的重点:counter 只 new 了一次,四个 taskpool 任务拿到的是同一个实例的引用。如果去掉 @Sendable,运行时会报"参数不可跨线程";如果去掉 AsyncLock,四个 worker 竞争 value += 1 会拿到少于 4000 的结果——共享内存意味着竞态可复现,这一点比 structured clone 时代更需要谨慎。
常规 Array<T> 不是 Sendable,扔进 taskpool 只能触发克隆。真正做到共享要用 @kit.ArkTS 的 collections 命名空间:
import { collections } from '@kit.ArkTS';
@Sendable
export class HotCache {
data: collections.Map<string, string> = new collections.Map();
put(k: string, v: string): void { this.data.set(k, v); }
get(k: string): string | undefined { return this.data.get(k); }
}差异在于 collections.Map 底层实现了跨线程内存屏障与迭代安全;Map / 普通 Array 只在当前 VM 隔离区可见。混用最常见的坑是:把普通 Array 塞到 @Sendable 类的字段里,编译器直接拒绝;而在方法内部临时构造普通数组再返回,则会在返回值路径上触发克隆,抵消了共享的意义。
用一个 10 万条记录的列表做基准(DevEco Profiler 采样,Mate 60 Pro,冷启动后稳定态平均值):
方案 | 主线程传递耗时 | 子线程读耗时 | 峰值内存 |
|---|---|---|---|
普通 Array<Record> + taskpool | ~180ms(结构化克隆) | ~4ms | 2× 数据大小 |
@Sendable + collections.Array | <1ms(引用传递) | ~5ms(首次跨线程访问轻微开销) | 1× 数据大小 |
结论清晰:数据越大,Sendable 的收益越接近线性放大;小对象反而看不出差别,此时用 structured clone 更简单。选型的分界线一般在"单次传递 > 1 万条 / 1MB"这个量级。
collections.*,否则运行时抛 BusinessError 10200201。@State、AppStorage 都不是 Sendable,禁止在 Sendable 类的方法里直接访问;应在主线程侧读出快照再传参。AsyncLock 或原子容器。把跨线程数据流分成三类,选型就清晰了:
collections,省克隆开销。AsyncLock,把并发正确性显式化到代码里。不要把 Sendable 当成"高性能默认选项"。它引入的心智负担是真实的:一旦对象跨线程共享,任何写操作都要重新审视竞态。工程上更好的做法是——先用 structured clone 跑通业务,等 Profiler 指认克隆成为瓶颈时再迁移到 Sendable。
Sendable 是 ArkTS 并发模型从"消息传递"走向"共享内存"的关键一步。它借助严格类型系统把可共享性做成了编译期契约,用 collections 容器把跨线程内存屏障做成了运行时能力,用 AsyncLock 把同步义务显式化。理解它的三条约束、掌握它的性能边界、遵守它的锁纪律,才能让多线程代码在鸿蒙上真正跑出该有的速度。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。