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 任务调用时直接引用
@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 这种机制,把临界区包起来,保证同一时间只有一个线程在改这个对象。
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();
}
}
加锁的范围要尽量小,只把真正需要互斥的代码包进去,不然并发性能就没了。不是所有共享对象都要全程加锁,只读的场景可以不加,读写混合的场景才需要。

六、工程上怎么判断该用哪种方式
写到这里其实可以总结一个判断思路了:
- 这个数据是不是只需要在后台任务里用一次?是 → 普通对象传参就行,复制就复制了。
- 后台任务需要读写主线程也在用的同一份数据?是 → 考虑 Sendable。
- 这个数据所有线程都要用,生命周期和应用一致?是 → 放进共享模块。
- 多个线程同时改同一份数据?是 → 必须加同步控制。
不要上来就把所有对象都标 Sendable,也不要把所有东西都塞进共享模块。并发设计的第一步是先想清楚"数据是复制、传递还是共享",然后再决定对象结构和同步方案。很多并发问题,其实在设计阶段就已经埋下了。

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