ArkTS Sendable 共享对象怎么用:TaskPool 里大对象别反复拷贝,冻结配置和进度回传怎么拆
2026/7/25 7:21:44 网站建设 项目流程

把计算丢进 TaskPool,本身不难。真正容易出问题的是:主线程传过去的对象到底是复制一份,还是多个任务看同一份?任务里能不能改?如果任务运行中要告诉页面“已经处理到第几批”,这个进度又该放在哪里?

这类问题如果前期没拆清楚,后面会出现很难排查的现象:数据量一大就慢,明明只是读配置却被多处改掉,同一个任务跑完以后 UI 进度和真实结果对不上。我的处理方式比较固定:输入配置尽量做成只读,任务只产出结果和进度事件,不在子任务里改共享输入。

先把几个概念放到同一张桌子上

名称在这个问题里负责什么
TaskPool适合短时间、可拆分、可并发的计算任务
Worker更适合常驻线程、长时间消息循环、持续通信
普通对象跨线程时通常按隔离内存传递,容易带来拷贝成本
Sendable用来表达可以在线程间共享的数据边界
freeze/只读约束让共享输入不能被任务偷偷改掉

这几个点不要分开看。只说 TaskPool,容易把所有东西都丢进去;只说 Sendable,又容易忽略任务生命周期;只说 freeze,又容易忘记进度和结果不能也一起冻住。比较稳的做法是:输入、进度、结果分三层。

问题一:大对象每个任务都复制一份,数据量上来就慢

先看一个常见场景:页面上有一批条目,要按规则做评分、过滤、分组。规则对象本身很大,里面可能有权重、黑名单、版本号、候选 ID。很多人一开始会直接把整包规则传给每个任务。

interfaceRankingRules{version:numberminScore:numberblockedTags:string[]weights:Record<string,number>recipeIds:string[]}@ConcurrentfunctionrankSlice(rules:RankingRules,sliceIndex:number):number{// 每个任务都拿到一份 rules,看起来简单,但大对象会被反复传递returnrules.recipeIds.filter((_,index)=>index%4===sliceIndex).length}

小数据量时这没什么感觉;一旦规则对象很大,任务又拆得多,问题就明显了:真正的计算可能不慢,慢的是任务启动前后的数据传递。

我更推荐把规则当作只读输入来处理。任务只读它,不改它;任务自己的输出单独返回。

@SendableclassRankingRulesSnapshot{version:number=0minScore:number=60blockedTags:string[]=[]weights:Record<string,number>={}recipeIds:string[]=[]freezeAfterBuild():RankingRulesSnapshot{Object.freeze(this.blockedTags)Object.freeze(this.weights)Object.freeze(this.recipeIds)Object.freeze(this)returnthis}}@ConcurrentfunctionrankSliceWithSnapshot(rules:RankingRulesSnapshot,sliceIndex:number):number{returnrules.recipeIds.filter((_,index)=>index%4===sliceIndex).length}

这里重点不是“用了一个装饰器就一定更快”,而是边界清楚了:规则快照是输入,任务结果是输出。输入不承担进度、不承担临时状态,也不承担 UI 展示状态。

问题二:任务想回传进度,顺手改了共享对象

另一个更隐蔽的问题是进度。比如一个任务处理到一半,想告诉页面“第 2 组完成了”。如果直接在共享对象里写finishedCount,后面很容易乱:多个任务同时写,顺序不稳定;某个任务失败了,输入对象还留着半截状态;下一轮任务复用对象时,旧进度没清掉。

不要这么写:

@SendableclassBadSharedState{recipeIds:string[]=[]finishedCount:number=0}@ConcurrentfunctionbadTask(state:BadSharedState,sliceIndex:number):number{constcount=state.recipeIds.filter((_,index)=>index%4===sliceIndex).length state.finishedCount+=countreturncount}

这个写法最麻烦的地方不是语法,而是职责混在了一起:recipeIds是输入,finishedCount是运行过程,最后返回值又是结果。一个对象同时扮演三个角色,后面一定难维护。

我会把进度单独做成事件。

interfaceProgressEvent{taskIndex:numberphase:'start'|'done'|'failed'accepted:number}interfaceSliceResult{taskIndex:numberaccepted:numberprogress:ProgressEvent}@ConcurrentfunctionrankSliceAndReport(rules:RankingRulesSnapshot,sliceIndex:number):SliceResult{constaccepted=rules.recipeIds.filter((_,index)=>index%4===sliceIndex).lengthreturn{taskIndex:sliceIndex,accepted,progress:{taskIndex:sliceIndex,phase:'done',accepted}}}

这样页面侧拿到结果后再合并:

asyncfunctionrunRanking(rules:RankingRulesSnapshot){consttasks=[0,1,2,3].map(index=>{returntaskpool.execute(newtaskpool.Task(rankSliceAndReport,rules,index))})constresults=awaitPromise.all(tasks)consttotal=results.reduce((sum,item)=>sum+item.accepted,0)constprogress=results.map(item=>item.progress)return{total,progress}}

这里有一个很实用的判断标准:只要某个字段会随着任务执行变化,就不要把它塞进共享输入对象。它应该是结果,或者是事件。

我本地用一个小脚本验证了边界

为了避免只停留在概念上,我用脚本模拟了两种方式:

  • 普通传递方式:4 个任务各自复制一份规则;
  • 只读共享方式:规则对象只构建一次,任务只读,进度单独返回。

运行结果是这样的:

{"ok":true,"cloneStyle":{"cloneCount":4,"totalAccepted":1200},"sharedReadonlyStyle":{"cloneCount":0,"totalAccepted":1200,"progressEvents":[{"taskIndex":0,"phase":"done","accepted":300},{"taskIndex":1,"phase":"done","accepted":300},{"taskIndex":2,"phase":"done","accepted":300},{"taskIndex":3,"phase":"done","accepted":300}]},"frozenCheck":{"mutationBlocked":true,"minScoreStill":60}}

这个验证说明三件事:

  1. 规则对象不应该被每个任务重复复制;
  2. 只读输入可以防止任务偷偷改配置;
  3. 进度事件和最终结果应该独立返回,不要污染输入对象。

几种方案怎么选

方案适合场景风险
普通对象直接传小对象、低频任务、逻辑简单数据大了容易有拷贝成本
Sendable 共享输入大对象、多任务复用、只读规则需要遵守 Sendable 约束
freeze 后共享配置、规则、只读快照不适合需要频繁变更的状态
Worker 常驻长时间通信、持续任务生命周期和消息协议更重

我的选择顺序一般是:

  • 只是一次很小的计算,用普通对象就够;
  • 规则对象很大,多个 TaskPool 任务都要读,用 Sendable 或共享快照;
  • 这个对象不应该被改,就在构建完成后冻结;
  • 如果需要长时间持续通信,再考虑 Worker。

可以封装成一个小工具

项目里可以把这套边界收成一个小的任务入口。

classParallelRankingRunner{asyncrun(rules:RankingRulesSnapshot,sliceCount:number):Promise<number>{consttasks=Array.from({length:sliceCount},(_,index)=>{returntaskpool.execute(newtaskpool.Task(rankSliceAndReport,rules,index))})constresults=awaitPromise.all(tasks)returnresults.reduce((sum,item)=>sum+item.accepted,0)}}

这样调用方只关心“我要跑几片、总结果是多少”,不用每个页面都重新想一遍线程间对象怎么传、进度怎么回、配置能不能改。

最后怎么避免踩坑

我会把检查项写成四条:

  • 输入对象只放输入,不放进度;
  • 共享输入能只读就只读;
  • 任务结果单独返回,失败信息也单独返回;
  • 大对象进入 TaskPool 前先判断有没有必要共享,别为了用新能力而用新能力。

Sendable 真正解决的不是“语法怎么写”,而是线程间数据边界怎么更清楚。边界清楚以后,TaskPool 代码会简单很多:输入稳定,任务独立,结果可合并,页面也不会被一堆临时状态拖乱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询