资讯动态

华为鸿蒙开发高级篇04-多线程与并发:TaskPool/Worker 实战

发布时间:2026/9/16 9:22:52 来源:尧图企业网站定制
华为鸿蒙开发高级篇04-多线程与并发TaskPool/Worker 实战图片批量压缩案例高级系列第 4 篇 · 案例相册批量压缩工具——一次选择 50 张图压缩、保存、进度展示UI 全程不卡顿。 目标搞清楚 ArkTS 并发模型TaskPool vs Worker掌握耗时任务下放子线程、线程间数据传递与进度回传的完整套路。一、案例背景图片压缩、大文件解析、模型推理这类 CPU 密集任务如果在主线程UI 线程执行结果就是点击后白屏、滑动掉帧、甚至 ANR。正确的做法是UI 线程只负责交互与渲染重活交给并发线程结果回传刷新。ArkTS 提供两套并发方案方案适用特点TaskPool一次性/短任务、可复用线程池自动调度、任务级并发、推荐首选Worker常驻长任务、需要独立生命周期手动创建/销毁、适合后台持续作业图片批量压缩是典型的短任务队列用 TaskPool 最合适文末给 Worker 的对照实现。二、核心原理TaskPool 工作方式主线程 ── execute(任务函数, 参数) ── TaskPool系统线程池 ── onReceiveData(进度) ─── 子线程回调 ── Promise结果 ───────── 任务完成要点任务函数必须是独立函数或独立 Concurrent 函数不能依赖闭包捕获主线程对象。传递的参数和返回值必须可序列化基本类型、数组、普通对象不能传自定义类实例/方法。进度回传用sendData/onReceiveData或直接返回聚合结果分批展示。三、实际开发代码图片批量压缩3.1 并发任务函数Concurrent// concurrency/compressTask.ets import { taskpool } from kit.ArkTS // Concurrent标记为可在线程池执行的函数 // 入参/返回值必须可序列化这里传图片路径 压缩参数 Concurrent function compressOne(arg: CompressArg): CompressResult { const { src, quality, index } arg // 真实场景读取 image source → 解码 → 压缩编码 → 写回文件 // 这里用伪实现模拟耗时与结果替换为实际图像编解码代码即可 const start Date.now() // 模拟压缩耗时 50~120ms while (Date.now() - start 60 (index % 60)) { // busy wait 模拟 CPU 密集 } const outPath src.replace(.jpg, _q${quality}.jpg) return { index, outPath, savedBytes: 1024 * (quality 60 ? 320 : 180) // 模拟压缩后大小 } } export interface CompressArg { src: string quality: number index: number } export interface CompressResult { index: number outPath: string savedBytes: number }3.2 批量调度TaskPool 并发限制 进度// service/CompressService.ets import { taskpool } from kit.ArkTS import { compressOne, CompressArg, CompressResult } from ../concurrency/compressTask export class CompressService { // 一次最多并发 3 个任务避免资源打满 private readonly maxConcurrent 3 // 返回进度回调onProgress(index, total) async compressAll( sources: string[], quality: number, onProgress: (done: number, total: number) void ): PromiseCompressResult[] { const results: CompressResult[] [] let done 0 // 任务队列用 Promise 手动控制并发数简单版信号量 const queue sources.map((src, index) { const arg: CompressArg { src, quality, index } return taskpool.execute(compressOne, arg).then((r) r as CompressResult) }) // 分批 await控制同时执行的个数 for (let i 0; i queue.length; i this.maxConcurrent) { const batch queue.slice(i, i this.maxConcurrent) const batchResults await Promise.all(batch) results.push(...batchResults) done batchResults.length onProgress(done, sources.length) // 进度回传 UI } return results } }说明上述分批 Promise是最易读的并发限制写法追求极致吞吐可改用计数信号量start 一批后立即补充。真实压缩代码请使用kit.ImageKitImagePacker/ImageSource完成解码与编码。3.3 UI 层进度展示 不卡顿// pages/CompressPage.ets import { CompressService } from ../service/CompressService Entry Component struct CompressPage { private service: CompressService new CompressService() // 模拟 50 张图片路径 private sources: string[] Array.from({ length: 50 }, (_, i) /data/img_${i 1}.jpg) State progress: number 0 State totalText: string State running: boolean false private async startCompress() { if (this.running) return this.running true this.progress 0 this.totalText 开始压缩... try { const results await this.service.compressAll(this.sources, 60, (done, total) { // 进度回调更新状态UI 自动刷新主线程空闲滑动依旧流畅 this.progress Math.round((done / total) * 100) this.totalText 已压缩 ${done}/${total} 张 }) this.totalText 完成共 ${results.length} 张本批省下约 ${this.sumBytes(results)} KB } catch (e) { this.totalText 压缩失败${(e as Error).message} } finally { this.running false } } private sumBytes(results: { savedBytes: number }[]): number { return Math.round(results.reduce((s, r) s r.savedBytes, 0) / 1024) } build() { Column({ space: 24 }) { Text(相册批量压缩).fontSize(24).fontWeight(FontWeight.Bold) // 进度环复用第 1 篇的自绘组件思路这里用系统组件 Progress({ value: this.progress, total: 100 }) .width(100%).color(#007DFF) Text(this.totalText).fontSize(14).fontColor(#86909C) Button(this.running ? 压缩中... : 开始压缩 50 张, { type: ButtonType.Capsule }) .width(100%) .enabled(!this.running) .onClick(() this.startCompress()) // 说明压缩期间可以来回拖动这个列表验证 UI 不卡 List() { ForEach(Array.from({ length: 20 }, (_, i) 滑动测试项 ${i 1}), (item: string) { ListItem() { Text(item).fontSize(14).padding(10) } }, (item: string) item) } .layoutWeight(1) .width(100%) } .padding(24) } }3.4 Worker 对照常驻任务场景TaskPool 之外的场景长驻后台、独立生命周期用 Worker// workers/workerDemo.ets —— 子线程入口 import { worker, ThreadWorkerGlobalScope } from kit.ArkTS const workerPort worker.workerPort as ThreadWorkerGlobalScope workerPort.onmessage (e) { const { src, quality } e.data // 执行压缩... const result { src, quality, ok: true } // 回传结果 workerPort.postMessage(result) } workerPort.onmessageerror () { workerPort.close() }// 主线程使用 import { worker } from kit.ArkTS const wk new worker.ThreadWorker(entry/ets/workers/workerDemo.ets) wk.onmessage (e) { /* 接收子线程结果 */ } wk.postMessage({ src: /data/a.jpg, quality: 60 }) // 用完释放 // wk.terminate()选型建议默认 TaskPool自动管理、代码简洁需要长期存活、手动控制生命周期的后台任务才上 Worker。四、优化与踩坑问题处理报taskpool execute 参数不可序列化传基本类型/可序列化对象自定义类实例不能直接传任务函数报 undefined并发函数必须独立定义或 Concurrent不能引用外部闭包变量进度回调不刷新回调里更新State若频率过高可节流如每 5 条一次并发开太大反而变慢压测找最优并发数本例 3~5压缩任务本身也吃 CPU/IOWorker 消息丢失postMessage 是异步的业务侧做好回调幂等异常走 onmessageerror 收尾真机内存暴涨分批处理 及时释放 ImageSource避免一次性解码全部大图五、小结与延伸并发选型短任务队列 → TaskPool长驻后台 → Worker。三要素Concurrent 独立任务函数、可序列化参数、进度回传回调/Promise。案例沉淀CompressService并发受限的批量任务调度器 进度 UI可直接套用于文件转换、批量下载、数据迁移等场景。延伸任务取消检查取消标志、任务优先级、与分布式第 8 篇结合做跨端任务下发。下一篇预告网络层架构实战——统一请求层设计拦截器/缓存/重试/取消新闻客户端案例。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价