适用版本:HarmonyOS 6.1(API 12)及以上
验证环境:Pura 90 Pro 模拟器(HarmonyOS 6.1.1,API 24)
关键 API@Sendable 装饰器、collections.Array<T>taskpool


前言

前两篇介绍了 TaskPool 和 Worker,它们传递数据都依赖序列化——对象被克隆一份送入子线程,子线程拿到的是副本。当数据量大(MB 级对象、大数组)时,序列化本身就会消耗可观的时间和内存。

@Sendable 提供了另一条路:标注了该装饰器的类,其实例可以以引用方式在并发实例间传递,不需要序列化,跨线程访问的是同一个对象。本文演示其正确写法和编译器规则。


核心规则

1. 属性类型必须全部是 Sendable 类型

类型 是否 Sendable
number / string / boolean / null / undefined
collections.Array<SendableType>
collections.Map<K, V>
@Sendable class 实例
普通 number[] / string[] ❌ 编译报错
普通 class 实例 ❌ 编译报错
函数 / 闭包 ❌ 编译报错

2. @Sendable 类不允许继承普通类

// ✅ 正确:继承另一个 @Sendable 类
@Sendable
class Base { id: number = 0 }

@Sendable
class Child extends Base { name: string = '' }

// ❌ 错误:继承普通类
class NormalBase {}
@Sendable
class WrongChild extends NormalBase {}  // 编译报错

完整代码

import { taskpool, collections } from '@kit.ArkTS'

// @Sendable 类:所有属性必须是 Sendable 类型
@Sendable
class DataPacket {
  id: number = 0
  label: string = ''
  precomputedSum: number = 0
  count: number = 0

  constructor(id: number, label: string, count: number) {
    this.id = id
    this.label = label
    this.count = count
    this.precomputedSum = count * (count - 1)  // 预计算 0+2+4+...
  }
}

// @Sendable 对象作为 TaskPool 参数(引用传递)
@Concurrent
function processPacket(packet: DataPacket, workerTag: string): string {
  const verify = packet.precomputedSum === packet.count * (packet.count - 1)
  return `[${workerTag}] id=${packet.id} label="${packet.label}" ` +
    `sum=${packet.precomputedSum} count=${packet.count} verify=${verify}`
}

// 对比:普通参数(序列化拷贝传递)
@Concurrent
function processPlain(id: number, label: string, sum: number,
    count: number, workerTag: string): string {
  const verify = sum === count * (count - 1)
  return `[${workerTag}] id=${id} label="${label}" ` +
    `sum=${sum} count=${count} verify=${verify} [plain copy]`
}

// 主线程:并发发送两个任务
const packet = new DataPacket(42, 'HarmonyOS-Sendable', 100000)

const [res1, res2] = await Promise.all([
  taskpool.execute(new taskpool.Task(processPacket, packet, 'Worker-A')),
  taskpool.execute(new taskpool.Task(processPlain,
    packet.id, packet.label, packet.precomputedSum, packet.count, 'Worker-B')),
])

模拟器运行截图

初始状态

Sendable 演示初始状态


执行完成

两路并发均成功执行,verify=true 证明数据在子线程中被正确读取。

Sendable 执行结果:两路均验证通过

实测结果:

方式 数据量 结果 耗时
@Sendable 引用传递 100,000 项(sum=9,999,900,000) verify=true 1ms
散列参数序列化 同上 verify=true [plain copy] 1ms

本例数据量较小,两者差异不显著。当传递 MB 级 collections.Array 时,@Sendable 的性能优势会显著体现。


使用 collections.Array<T> 传递大数组

如需跨线程传递大数组,使用 collections.Array 替代普通 Array

import { collections, taskpool } from '@kit.ArkTS'

@Sendable
class BigData {
  items: collections.Array<number> = new collections.Array<number>()
}

@Concurrent
function sumItems(data: BigData): number {
  let total = 0
  for (let i = 0; i < data.items.length; i++) {
    total += data.items[i]
  }
  return total
}

// 填充 100 万项
const data = new BigData()
for (let i = 0; i < 1_000_000; i++) {
  data.items.push(i)
}

// 引用传递,无需序列化 100 万项数据
const result = await taskpool.execute(new taskpool.Task(sumItems, data))

常见问题

Q:@Sendable 类能用 number[] 吗?
A:不能,编译器会报 arkts-sendable-prop-types 错误。必须改用 collections.Array<number>。这是 ArkTS 的静态类型安全保证,防止普通数组在多线程下被无保护地并发修改。

Q:@Sendable 对象在子线程中修改后,主线程能看到变化吗?
A:能看到引用传递的对象修改,但需要注意并发安全:多个线程同时写同一个字段会有数据竞争。建议子线程只读 @Sendable 对象,写操作回传给主线程处理。

Q:TaskPool 和 Worker 都支持 @Sendable 吗?
A:都支持。TaskPool 的 @Concurrent 函数参数可以是 @Sendable 对象;Worker 的 postMessage 同样支持传递 @Sendable 对象。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐