首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >[鸿蒙从零到一] ArkTS Sendable 数据模型与跨线程共享实战

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

原创
作者头像
hunter android
修改2026-08-31 10:20:32
修改2026-08-31 10:20:32
180
举报

为什么需要 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

关键约束有三条:

  1. 字段必须显式声明类型,不允许 any / unknown / 联合含非 Sendable 类型。
  2. 方法内不能捕获非 Sendable 的外部变量(闭包被静态检查器拒绝)。
  3. 并发访问不是自动加锁,共享 = 可见性;一致性需要开发者自己用 ArkTSUtils.locks.AsyncLock 或 CAS 语义保证。

这三条决定了 Sendable 是"零拷贝但有代价"的模型:省下克隆开销,换来对并发正确性的显式管理。

最小可运行示例:跨线程共享一个计数器

代码语言:javascript
复制
// 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<T> 不是 Sendable,扔进 taskpool 只能触发克隆。真正做到共享要用 @kit.ArkTScollections 命名空间:

代码语言:javascript
复制
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(结构化克隆)

~4ms

2× 数据大小

@Sendable + collections.Array

<1ms(引用传递)

~5ms(首次跨线程访问轻微开销)

1× 数据大小

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

常见踩坑清单

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

方法论:什么时候该用 Sendable

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

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

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

小结

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 为什么需要 Sendable
  • 原理:Sendable 到底做了什么
  • 最小可运行示例:跨线程共享一个计数器
  • Sendable 容器 vs 普通数组
  • 性能对比:克隆 vs 共享
  • 常见踩坑清单
  • 方法论:什么时候该用 Sendable
  • 小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档