HarmonyOS 7 ArkTS 并发实战:Sendable、共享模块与跨线程对象传递机制

跨线程传对象之前,先想清楚它是复制、传递还是共享。

主线程里有个业务对象,里面存着用户配置和缓存数据,直接丢进 TaskPool 去跑后台计算,跑着跑着就出问题了:主线程改了一下配置,后台任务读到的还是旧值;或者后台任务改完返回,主线程发现对象结构不对。

这不是 TaskPool 的 bug,是 ArkTS 并发模型的边界和普通 JavaScript 不一样。JavaScript 里单线程跑,对象随便传随便改;到了 ArkTS 里,线程是隔离的,跨线程传对象有一套自己的规则。这篇就把 Sendable、共享模块和跨线程对象传递这件事捋清楚。

一、主线程对象放进 TaskPool 以后为什么不能随便改

先讲最直观的问题。把一个普通对象丢进 TaskPool 任务函数里,这个对象是怎么过去的?答案是:复制

不是把原对象的引用直接传过去,而是在后台线程里生成一个副本。后台线程改这个副本,不会影响主线程的原对象;反过来,主线程改原对象,后台线程读到的也还是当时复制过去的那个快照。

很多人写代码的时候默认"传的是引用",结果就会出现:我明明改了配置,后台任务怎么还用旧的?跑了半天才反应过来,后台线程拿到的根本不是那个对象本身。

传递方式 行为 适用场景
普通对象传参 深拷贝,跨线程独立副本 一次性计算,不需要共享状态
Sendable 对象 引用传递,多线程共享同一个实例 需要多线程读写同一份数据
共享模块 模块级变量全局共享 跨线程需要访问的公共配置、缓存

二、Sendable 到底解决了什么问题

Sendable 就是用来解决"跨线程共享对象"这件事的。被 @Sendable 装饰的类,它的实例可以被多个线程同时持有引用,而不是每次都复制一份。

适合做成 Sendable 的对象有什么特点?首先它的状态是多个线程都需要读写的,比如全局配置、共享缓存、任务队列里的数据项。其次它不能太复杂,Sendable 对类的结构是有约束的,不是随便哪个类加个装饰器就行。

代码放在 entry/src/main/model/TaskConfig.ets

这段代码解决什么问题: 定义一个可以跨线程共享的配置对象,避免每次传参都复制。

文件: entry/src/main/model/TaskConfig.ets

用途: 跨线程共享的任务配置

接入位置: TaskPool 任务调用时直接引用

ts 复制代码
@Sendable
export class TaskConfig {
  maxThread: number = 4;
  timeout: number = 5000;
  updatedAt: number = Date.now();

  update(newTimeout: number) {
    this.timeout = newTimeout;
    this.updatedAt = Date.now();
  }
}

这个类被标记为 Sendable 之后,主线程创建的实例可以直接传给 TaskPool,后台线程拿到的是同一个对象的引用。主线程改了 timeout,后台线程下一次读到的就是新值。

但这里有个关键前提:能共享不等于可以无锁修改。多个线程同时读写同一个字段的时候,还是会有竞争问题。

三、use shared 共享模块是干什么的

共享模块和 Sendable 又不太一样。Sendable 是"这个类的实例可以跨线程共享",而 use shared 是"这个模块本身就是共享的"。

共享模块里的变量、函数,所有线程都能访问。适合放什么?跨线程都需要的常量、全局配置、工具函数。比如应用启动时从 Preferences 读出来的配置,初始化之后放到共享模块里,所有后台线程直接用,不用每次都传参。

但共享模块的边界要划清楚:不是什么都往里面塞。临时数据、页面级状态、带 UI 上下文的对象,都不应该放进共享模块。共享模块里应该是"所有线程都需要、生命周期和应用一致"的东西。

四、TaskPool 和 Worker 到底有什么不一样

这两个经常被放在一起对比,但它们的定位不一样。

TaskPool 适合"短任务、一次性计算":丢一个任务进去,跑完拿到结果。它自动管理线程池,不用你手动管线程生命周期。传参的时候,普通对象复制,Sendable 对象共享。

Worker 适合"长连接、持续通信":创建一个 Worker 实例,主线程和它之间通过消息来回传数据,适合需要持续交互的场景。

对比项 TaskPool Worker
任务模式 短任务,自动调度 长连接,手动管理
通信方式 函数调用 + 返回值 postMessage 双向通信
对象传递 普通复制/Sendable 共享 结构化克隆
适用场景 批量计算、数据处理 持续运行的后台服务

五、共享了为什么还要加锁

很多人以为用了 Sendable 就万事大吉了,多线程同时读写同一个对象,不就是共享吗?

错。共享解决的是"多个线程能看到同一份数据",但没解决"多个线程同时改的时候会不会出问题"。两个线程同时改同一个字段,一个读一个写,中间的状态就可能是错的。

这时候就需要同步控制。ArkTS 里可以用 AsyncLock 这种机制,把临界区包起来,保证同一时间只有一个线程在改这个对象。

ts 复制代码
import { AsyncLock } from '@ohos.taskpool';

const lock = new AsyncLock();

async function updateConfig(config: TaskConfig, newTimeout: number) {
  await lock.acquire();
  try {
    config.update(newTimeout);
  } finally {
    lock.release();
  }
}

加锁的范围要尽量小,只把真正需要互斥的代码包进去,不然并发性能就没了。不是所有共享对象都要全程加锁,只读的场景可以不加,读写混合的场景才需要。

六、工程上怎么判断该用哪种方式

写到这里其实可以总结一个判断思路了:

  1. 这个数据是不是只需要在后台任务里用一次?是 → 普通对象传参就行,复制就复制了。
  2. 后台任务需要读写主线程也在用的同一份数据?是 → 考虑 Sendable。
  3. 这个数据所有线程都要用,生命周期和应用一致?是 → 放进共享模块。
  4. 多个线程同时改同一份数据?是 → 必须加同步控制。

不要上来就把所有对象都标 Sendable,也不要把所有东西都塞进共享模块。并发设计的第一步是先想清楚"数据是复制、传递还是共享",然后再决定对象结构和同步方案。很多并发问题,其实在设计阶段就已经埋下了。

这套做下来最大的体会是:ArkTS 的并发模型不是 JavaScript 单线程的简单延伸,线程隔离和对象传递的规则一开始就要想清楚。对象传过去是副本还是引用,共享了要不要加锁,这些问题在写代码之前就要有答案,不然跑起来出了问题排查起来会非常绕。

相关推荐
骑着蜗牛撵大象3273 小时前
从跨平台Linux到鸿蒙PC:基于Qt C++的Flameshot截图工具源码级适配
harmonyos·鸿蒙·es
威哥爱编程5 小时前
HarmonyOS 7 空间音频实战:降噪、美化、变声、空间渲染的节点编排
华为·harmonyos·arkts
威哥爱编程5 小时前
HarmonyOS 7 视觉 AI 实战:系统级场景化控件,低门槛接入端侧视觉能力
华为·harmonyos·arkts
贾伟康5 小时前
【HarmonyOS 7新能力|046】智慧手势工程封装:把接入逻辑放进可维护的分层结构
人机交互·harmonyos·arkts·arkui·手势识别
贾伟康5 小时前
【HarmonyOS 7新能力|050】Core File Kit mmap工程封装:把接入逻辑放进可维护的分层结构
性能优化·harmonyos·arkts·文件系统·mmap
贾伟康5 小时前
【HarmonyOS 7新能力|048】Taihe IPC工程封装:把接入逻辑放进可维护的分层结构
harmonyos·arkts·ipc·分布式通信·taihe
贾伟康5 小时前
【HarmonyOS 7新能力|045】LazyLayoutAlgorithm工程封装:把接入逻辑放进可维护的分层结构
性能优化·harmonyos·arkts·arkui·懒加载
梦想不只是梦与想15 小时前
鸿蒙 云测试:上架测试流程
harmonyos·鸿蒙·云测试
传奇开心果编程18 小时前
【ArkUI 练中学】第15课:UI 界面设计与实战
学习·ui·华为·harmonyos