ArkTS TaskPool:超分批次签名过期结果隔离【鸿蒙心迹】
给一批图片排队做超分时,界面最容易掩盖的并不是计算慢,而是上一批的计算结果比下一批更晚回来。用户先选了三张图,随后修改任务参数,页面已经显示新的批次,后台却还可能交回旧任务的校验信息。如果只写 await taskpool.execute(...),回调顺序就可能替你决定当前状态。这篇文章只聚焦任务清单的轻量签名计算与回包归属,不实现模型推理,更不把示例签名说成图像质量评分。

一、先定义“哪批结果有资格上屏”
演示工程叫 BatchStamp,任务号 BS-029,当前有效批次 B-06。清单中有三个业务 ID:SR-21、SR-22、SR-23。另一份先前提交的批次叫 B-05。为了说明回包竞争,我们预设先提交 B-05,后提交 B-06,但 B-06 首先返回;页面应该只接收 B-06,丢弃之后到达的 B-05。这不是一次真实并发压测,而是一条可以用受控延迟复现的逻辑序列。
真正的超分链路至少还包含图片读取、模型能力调用、产物生成和资源清理。这些环节的 API、输入上限和设备能力必须由具体引擎决定。本文只做推理开始前的清单签名:它能够发现清单变更、给批次建立可追踪标识,却不能证明图片内容一样,更不能替代文件哈希、像素校验或模型输出正确性。
为什么需要两种 ID?BS-029 是用户可见的工作任务,跨多次配置修改仍可以保持不变;B-06 则是一次配置快照的版本。两者混用后,日志中就很难判断晚到回包属于哪次计算。这里规定只要批次变化,代次号也必须增加;界面允许显示最新批次的签名,旧批次只能进入诊断记录。
二、把纯计算留在 TaskPool 的边界内
官方 TaskPool 指导推荐用 @Concurrent 声明可分发的任务函数,再通过 taskpool.Task 和 taskpool.execute() 执行。示例刻意只传入字符串数组,不把 PixelMap、页面组件、数据库连接或原生推理句柄直接传给任务池。这样既能说明并发基础结构,也避开复杂对象跨线程可传输性的额外限制。
下面的签名函数对 ID 串使用一个简单的滚动余数。计算规则固定为按顺序拼接 SR-21|SR-22|SR-23,逐字符 signature=(signature*31+字符码)%9973,演示结果为 6949。它只是一个可读的教学校验码,没有抗碰撞性,不可用于文件完整性、安全认证和正式缓存命中判断。代码解决的是“把轻量计算与当前 UI 生命周期解耦”的第一步。
import { taskpool } from '@kit.ArkTS';
@Concurrent
export function rollingSignature(ids: string[]): number {
const joined: string = ids.join('|');
let signature: number = 0;
for (let i: number = 0; i < joined.length; i++) {
signature = (signature * 31 + joined.charCodeAt(i)) % 9973;
}
return signature;
}
export async function signBatch(ids: string[]): Promise<number> {
const snapshot: string[] = ids.slice();
const task: taskpool.Task = new taskpool.Task(rollingSignature, snapshot);
const value = await taskpool.execute(task);
return value as number;
}
这段代码不依赖页面状态,返回值只有一个数字,故意减少了线程通信复杂度。slice() 取得一份清单快照;如果后面允许用户拖动任务顺序,就要先决定顺序是否参与签名。现在它参与,所以同一组三张图换一个排列也可能产生不同签名。未来若业务语义是集合而非队列,应在快照时排序,但不能随意改变现有签名规则,否则历史记录无法比较。
还需要提醒一点:TaskPool 是任务调度机制,不是保证后台运行时长的许可。任务被分发到工作线程不意味着应用退后台后就能无限执行,更不能把它当成系统长期后台任务能力。模型推理是否适合 TaskPool,要看引擎是否支持线程间对象传递和实例生命周期,并结合官方能力边界重新设计。
三、完成得早不等于可以提交得早
接下来把“什么时候返回”和“是否仍属于当前批次”分开。以下控制器只保留必要状态:latestSeq 表示当前有效代次,alive 表示页面能否继续接收结果;acceptedBatch、signature 用于展示。每次提交先递增代次,再等待结果。无论 B-05、B-06 谁先完成,都必须经过代次比对才能更新页面。这段代码解决的是过期 Promise 覆盖新 UI,不是中断正在运行的工作线程。
class BatchGate {
latestSeq: number = 0;
alive: boolean = true;
acceptedBatch: string = '';
signature: number = 0;
async submit(batchId: string, ids: string[]): Promise<string> {
const mySeq: number = ++this.latestSeq;
try {
const value: number = await signBatch(ids);
if (!this.alive || mySeq !== this.latestSeq) {
return `DROP ${batchId} stale`;
}
this.acceptedBatch = batchId;
this.signature = value;
return `ACCEPT ${batchId} signature=${value}`;
} catch (error) {
if (!this.alive || mySeq !== this.latestSeq) return 'DROP stale error';
return `ERROR ${batchId}: ${String(error)}`;
}
}
invalidate(): void {
this.alive = false;
++this.latestSeq;
}
}
不要把 mySeq 当作 TaskPool 自带的任务 ID;它是业务层生成的递增序号。即使系统执行完成,mySeq !== latestSeq 时也不能再写当前页。最容易漏的是失败分支:旧批次如果在新批次之后报错,同样不能把错误横幅覆盖当前成功态。因此 catch 里也做代次校验。
alive=false 代表当前控制器退出消费阶段,并非任务已经被物理取消。页面重进时应重新创建控制器,不能把已经失效的对象简单设置回 true,否则还没返回的旧任务可能重新得到写入许可。复杂工程应把 TaskPool 任务句柄纳入专门队列管理,并评估 taskpool.cancel() 对不同执行阶段的实际行为;已经运行的计算是否可协作停止,需要根据任务特性明确,不应凭取消调用直接宣称结束。
四、把业务日志设计成可复盘的因果序列
为了观察门禁是否生效,BatchStamp 预设四个关键事件:21:40:21 dispatch=B-05 ids=3、21:40:22 dispatch=B-06 ids=3、21:40:23 accept=B-06 signature=6949、21:40:24 drop=B-05 stale。这四行仅表示设计好的事件顺序,不是运行环境真实 HiLog。观察点不是哪一秒更快,而是最后一次提交的版本才拥有写入权。
在页面上显示 B-06、三张任务卡片与 6949,但不显示所谓“超分完成耗时”“画质提升倍率”等没有测量的数据。签名只覆盖任务 ID;实际的超分参数——倍率、模型版本、目标尺寸、图像 URI 的有效性——如果会影响产物,也必须纳入正式批次指纹。这里先不虚构这些字段的 SDK 语义。
为了处理页面销毁与重新进入,组件层只持有当前控制器,并在消失时使其失效。以下是第三段核心代码,用来明确 UI 到业务门禁的接线,以及 @State 只能由仍有效的结果更新的原则。
@Entry
@Component
struct BatchStampPage {
@State statusText: string = 'B-06 · 待校验';
private gate: BatchGate = new BatchGate();
private readonly ids: string[] = ['SR-21', 'SR-22', 'SR-23'];
aboutToAppear(): void {
this.gate = new BatchGate();
}
async verifyCurrent(): Promise<void> {
const result: string = await this.gate.submit('B-06', this.ids);
if (this.gate.alive && result.startsWith('ACCEPT')) {
this.statusText = 'B-06 · 签名 6949';
}
}
aboutToDisappear(): void {
this.gate.invalidate();
}
build() {
Column({ space: 12 }) {
Text('BatchStamp · BS-029')
Text(this.statusText)
Button('校验当前批次').onClick(() => { this.verifyCurrent(); })
}.padding(16)
}
}
这里的 verifyCurrent() 只演示单次 B-06 的入口,前述 B-05/B-06 乱序需要测试用的受控延迟驱动来构造,不能从这个按钮代码推断已经测到乱序。实际工程还要考虑同一页反复点击产生多个 B-06 请求的问题:若不希望重复提交,应禁用按钮,或者把同一批次的请求做合并,并在失败后允许显式重试。示例省略了这些产品细节,避免把一个小门禁写成整个任务平台。

五、用三种失败路径衡量方案的完整度
第一种是“旧批次成功得很晚”。只要 B-05 晚于 B-06,就应该记录 DROP B-05,但不能覆盖 B-06 的签名。第二种是“旧批次在新批次成功后抛异常”。页面不应显示过期错误,但诊断日志仍应说明异常来自哪个批次。第三种是“用户在签名任务返回之前退出页面”。这时可以保留任务内部的计算记录,却不能访问已经消失的页面对象或弹出一个迟到的成功提示。
演示截图把 B-06、SR-21、SR-22、SR-23、签名 6949、失效的 B-05 放在一张屏幕里,时间统一为 21:40。图里没有真实超分产物,只有任务清单校验结果。它的作用是校对文图字段,而不是证明耗时、线程数或后台续航水平。

如果要进一步测试,我会在 DevEco 中先让两个批次按顺序完成,再人为给旧批次增加等待,让它最后返回,然后把页面退出插在中间。每次记录 seq、业务 batchId、任务数、签名与消费决策,不要只打印“TaskPool success”。把三种路径都打通以后,才有资格讨论能否接入真正的模型调用与资源回收。
六、停止在哪里,也是架构的一部分
这套方案解决的是“哪一个异步结果有权更新当前页面”。它不能代替真正的任务取消、图片解码释放、模型资源复用、超分效果验证或设备侧性能评测。校验码 6949 不代表任何安全保证;如果要做正式去重与产物缓存,应根据清单和参数采用合适的哈希算法,并控制输入的可复现性。
更重要的是别混淆“任务完成”和“页面仍需要它”。一个可靠的鸿蒙应用可以让旧任务计算完成,但拒绝消费其结果;也可以在支持安全取消的条件下回收队列工作。两者不是同一件事。官方 TaskPool 文档明确了 @Concurrent、Task 与 execute() 的使用方式及与 Worker 的差异,跨线程对象约束也值得对照阅读。本文未在真实设备执行签名任务,也不主张通过模拟器画面得出 TaskPool 实机结论。
更多推荐





所有评论(0)