ArkTS Sendable 共享对象怎么用:TaskPool 里大对象别反复拷贝,冻结配置和进度回传怎么拆

📅 2026/7/25 7:21:52
ArkTS Sendable 共享对象怎么用:TaskPool 里大对象别反复拷贝,冻结配置和进度回传怎么拆
把计算丢进 TaskPool本身不难。真正容易出问题的是主线程传过去的对象到底是复制一份还是多个任务看同一份任务里能不能改如果任务运行中要告诉页面“已经处理到第几批”这个进度又该放在哪里这类问题如果前期没拆清楚后面会出现很难排查的现象数据量一大就慢明明只是读配置却被多处改掉同一个任务跑完以后 UI 进度和真实结果对不上。我的处理方式比较固定输入配置尽量做成只读任务只产出结果和进度事件不在子任务里改共享输入。先把几个概念放到同一张桌子上名称在这个问题里负责什么TaskPool适合短时间、可拆分、可并发的计算任务Worker更适合常驻线程、长时间消息循环、持续通信普通对象跨线程时通常按隔离内存传递容易带来拷贝成本Sendable用来表达可以在线程间共享的数据边界freeze/只读约束让共享输入不能被任务偷偷改掉这几个点不要分开看。只说 TaskPool容易把所有东西都丢进去只说 Sendable又容易忽略任务生命周期只说 freeze又容易忘记进度和结果不能也一起冻住。比较稳的做法是输入、进度、结果分三层。问题一大对象每个任务都复制一份数据量上来就慢先看一个常见场景页面上有一批条目要按规则做评分、过滤、分组。规则对象本身很大里面可能有权重、黑名单、版本号、候选 ID。很多人一开始会直接把整包规则传给每个任务。interfaceRankingRules{version:numberminScore:numberblockedTags:string[]weights:Recordstring,numberrecipeIds:string[]}ConcurrentfunctionrankSlice(rules:RankingRules,sliceIndex:number):number{// 每个任务都拿到一份 rules看起来简单但大对象会被反复传递returnrules.recipeIds.filter((_,index)index%4sliceIndex).length}小数据量时这没什么感觉一旦规则对象很大任务又拆得多问题就明显了真正的计算可能不慢慢的是任务启动前后的数据传递。我更推荐把规则当作只读输入来处理。任务只读它不改它任务自己的输出单独返回。SendableclassRankingRulesSnapshot{version:number0minScore:number60blockedTags:string[][]weights:Recordstring,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%4sliceIndex).length}这里重点不是“用了一个装饰器就一定更快”而是边界清楚了规则快照是输入任务结果是输出。输入不承担进度、不承担临时状态也不承担 UI 展示状态。问题二任务想回传进度顺手改了共享对象另一个更隐蔽的问题是进度。比如一个任务处理到一半想告诉页面“第 2 组完成了”。如果直接在共享对象里写finishedCount后面很容易乱多个任务同时写顺序不稳定某个任务失败了输入对象还留着半截状态下一轮任务复用对象时旧进度没清掉。不要这么写SendableclassBadSharedState{recipeIds:string[][]finishedCount:number0}ConcurrentfunctionbadTask(state:BadSharedState,sliceIndex:number):number{constcountstate.recipeIds.filter((_,index)index%4sliceIndex).length state.finishedCountcountreturncount}这个写法最麻烦的地方不是语法而是职责混在了一起recipeIds是输入finishedCount是运行过程最后返回值又是结果。一个对象同时扮演三个角色后面一定难维护。我会把进度单独做成事件。interfaceProgressEvent{taskIndex:numberphase:start|done|failedaccepted:number}interfaceSliceResult{taskIndex:numberaccepted:numberprogress:ProgressEvent}ConcurrentfunctionrankSliceAndReport(rules:RankingRulesSnapshot,sliceIndex:number):SliceResult{constacceptedrules.recipeIds.filter((_,index)index%4sliceIndex).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))})constresultsawaitPromise.all(tasks)consttotalresults.reduce((sum,item)sumitem.accepted,0)constprogressresults.map(itemitem.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}}这个验证说明三件事规则对象不应该被每个任务重复复制只读输入可以防止任务偷偷改配置进度事件和最终结果应该独立返回不要污染输入对象。几种方案怎么选方案适合场景风险普通对象直接传小对象、低频任务、逻辑简单数据大了容易有拷贝成本Sendable 共享输入大对象、多任务复用、只读规则需要遵守 Sendable 约束freeze 后共享配置、规则、只读快照不适合需要频繁变更的状态Worker 常驻长时间通信、持续任务生命周期和消息协议更重我的选择顺序一般是只是一次很小的计算用普通对象就够规则对象很大多个 TaskPool 任务都要读用 Sendable 或共享快照这个对象不应该被改就在构建完成后冻结如果需要长时间持续通信再考虑 Worker。可以封装成一个小工具项目里可以把这套边界收成一个小的任务入口。classParallelRankingRunner{asyncrun(rules:RankingRulesSnapshot,sliceCount:number):Promisenumber{consttasksArray.from({length:sliceCount},(_,index){returntaskpool.execute(newtaskpool.Task(rankSliceAndReport,rules,index))})constresultsawaitPromise.all(tasks)returnresults.reduce((sum,item)sumitem.accepted,0)}}这样调用方只关心“我要跑几片、总结果是多少”不用每个页面都重新想一遍线程间对象怎么传、进度怎么回、配置能不能改。最后怎么避免踩坑我会把检查项写成四条输入对象只放输入不放进度共享输入能只读就只读任务结果单独返回失败信息也单独返回大对象进入 TaskPool 前先判断有没有必要共享别为了用新能力而用新能力。Sendable 真正解决的不是“语法怎么写”而是线程间数据边界怎么更清楚。边界清楚以后TaskPool 代码会简单很多输入稳定任务独立结果可合并页面也不会被一堆临时状态拖乱。