[鸿蒙从零到一] ArkTS Sendable 数据模型与跨线程共享实战

为什么需要 Sendable

ArkTS 的多线程模型(taskpool、Worker)不像 JVM 那样共享堆内存。跨线程默认走结构化克隆(structured clone),代价是拷贝开销 + 引用断裂:主线程改了一份,子线程看不到。业务里但凡涉及大列表、缓存、状态机在多线程之间流动,克隆语义就成了性能与一致性的双重瓶颈。 Sendable 数据模型的定位就是让一份对象真正跨线程共享,而不是每次传递都深拷贝。它是 ArkTS 静态类型系统 + 运行时并发原语共同支撑出来的机制,本质上把"可跨线程"变成了一种编译期能校验的对象契约。

原理:Sendable 到底做了什么

Sendable 对象在运行时被分配到一个跨线程可访问的堆区域(Shared Heap)。它的字段类型受限,只能是:

  • 基础类型(number / string / boolean / bigint / null / undefined)
  • 其他 Sendable 类实例
  • Sendable 容器:collections.Arraycollections.Mapcollections.Set

关键约束有三条:

  • 字段必须显式声明类型,不允许 any / unknown / 联合含非 Sendable 类型。
  • 方法内不能捕获非 Sendable 的外部变量(闭包被静态检查器拒绝)。
  • 并发访问不是自动加锁 ,共享 = 可见性;一致性需要开发者自己用 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 时代更需要谨慎。

Sendable 容器 vs 普通数组

常规 Array&lt;T&gt; 不是 Sendable,扔进 taskpool 只能触发克隆。真正做到共享要用 @kit.ArkTScollections 命名空间: `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` 类的字段里,编译器直接拒绝;而在方法内部临时构造普通数组再返回,则会在返回值路径上触发克隆,抵消了共享的意义。

性能对比:克隆 vs 共享

用一个 10 万条记录的列表做基准(DevEco Profiler 采样,Mate 60 Pro,冷启动后稳定态平均值):

方案主线程传递耗时子线程读耗时峰值内存

普通 Array<Record> + taskpool~180ms(结构化克隆)~4ms2× 数据大小 @Sendable + collections.Array<1ms(引用传递)~5ms(首次跨线程访问轻微开销)1× 数据大小

结论清晰:数据越大,Sendable 的收益越接近线性放大;小对象反而看不出差别,此时用 structured clone 更简单。选型的分界线一般在"单次传递 > 1 万条 / 1MB"这个量级。

常见踩坑清单

  • 忘记标 @Sendable:类字段互相引用时,只要有一个漏标,编译器就会把整棵引用图判为不可跨线程。
  • 字段用了普通 Array/Map :换成 collections.*,否则运行时抛 BusinessError 10200201
  • 方法内捕获 UI 状态@StateAppStorage 都不是 Sendable,禁止在 Sendable 类的方法里直接访问;应在主线程侧读出快照再传参。
  • 忽略锁 :Sendable = 共享内存,不 = 自动同步;写操作必须走 AsyncLock 或原子容器。
  • 把 Sendable 对象持久化:Preferences / relationalStore 不识别 Sendable 元数据,落盘前要转成 POJO 或 JSON。

方法论:什么时候该用 Sendable

把跨线程数据流分成三类,选型就清晰了:

  • 一次性任务参数(< 100KB,用完即抛):直接 structured clone,代码最简单。
  • 大批量只读数据 (列表、字典、模型输入):Sendable + collections,省克隆开销。
  • 可变共享状态 (缓存、计数器、状态机):Sendable + AsyncLock,把并发正确性显式化到代码里。

不要把 Sendable 当成"高性能默认选项"。它引入的心智负担是真实的:一旦对象跨线程共享,任何写操作都要重新审视竞态。工程上更好的做法是------先用 structured clone 跑通业务,等 Profiler 指认克隆成为瓶颈时再迁移到 Sendable

小结

Sendable 是 ArkTS 并发模型从"消息传递"走向"共享内存"的关键一步。它借助严格类型系统把可共享性做成了编译期契约,用 collections 容器把跨线程内存屏障做成了运行时能力,用 AsyncLock 把同步义务显式化。理解它的三条约束、掌握它的性能边界、遵守它的锁纪律,才能让多线程代码在鸿蒙上真正跑出该有的速度。

相关推荐
Csvn38 分钟前
模块化与工程化:CJS / ESM / 打包器,到底怎么协作?
前端
AICoder码农王1 小时前
Vitest 实战:从一个真实项目看懂测试套件
前端·前端框架
律宏阔1 小时前
Electron Forge 在 macOS M4 打包踩坑:Node.js 26 导致 make 异常中断
前端
木木爱研究1 小时前
elpis-里程碑四-基于Vue完成动态组件库建设
前端·后端
深念Y1 小时前
Nuxt 项目 Docker 构建:从 pnpm+node 迁移到全 bun
java·前端·docker
qq_426003961 小时前
UI自动化元素定位不到解决方案
前端·pycharm·自动化
EthanChou20201 小时前
AES67协议笔记
前端·笔记·es6
马优晨1 小时前
pom.xml 中 jquery webjars 依赖解释
xml·前端·jquery·jquery webjars·webjars依赖
里欧跑得慢2 小时前
AI 驱动的 UI 国际化方案自动适配:从单一语言到多语言布局的智能转换
前端·css·flutter·web