当前位置: 首页 > news >正文

HarmonyOS7 多线程:Worker 让耗时任务不再卡 UI

文章目录

      • 前言
      • 为什么需要多线程
      • Worker vs TaskPool
      • Worker 创建与通信
      • 耗时计算实战
      • 线程间数据传递(序列化)
      • 线程池管理
      • 写在最后

前言

你有没有遇到过这种情况:列表页加载 5000 条数据,排序逻辑一跑,整个页面直接冻住?用户疯狂点击屏幕没反应,心想这 App 怎么跟死机了一样。

根本原因就是耗时任务占了主线程,UI 没机会刷新。解决方案很简单——把耗时任务扔到子线程去跑,Worker 就是干这个的。

JavaScript 天生是单线程的,ArkTS 也没逃掉这个基因。但 HarmonyOS7 给了两个多线程方案:Worker 和 TaskPool。TaskPool 用起来更简单,但 Worker 更灵活、更适合长任务。

说实话,如果你只是想跑个简单计算,TaskPool 够用了。但如果你的任务要跑几十分钟、需要维护状态、要跟主线程频繁通信——Worker 才是正解。今天重点聊 Worker。

为什么需要多线程

主线程只有一个职责:刷新 UI。所有用户能看到的东西,都在主线程上渲染。

一旦你在主线程干重活——大量计算、文件读写、JSON 解析大对象——UI 就卡了。而且 HarmonyOS 对主线程有超时检测,阻塞超过一定时间直接 ANR(应用无响应)。

所以规则很简单:跟 UI 无关的活,统统扔到子线程。

Worker vs TaskPool

很多人分不清这俩,先来张对比表:

维度WorkerTaskPool
线程生命周期开发者手动管理系统自动管理
任务粒度整个线程级别单个任务级别
长任务支持✅ 可以跑几小时❌ 超过 3 分钟会被系统回收
状态维护✅ 线程内可以维护持久状态❌ 任务结束状态就没了
优先级控制✅ API 18+ 支持✅ 支持
任务取消需自己实现✅ 原生支持cancel()
负载均衡❌ 手动管理✅ 自动扩缩容
创建开销较大,需要新建线程小,复用线程池
通信方式postMessage/onmessage任务返回值

一句话总结:短任务用 TaskPool,长任务用 Worker。不确定就用 TaskPool,省心。

Worker 创建与通信

Worker 的通信模型就是经典的Actor 模型——主线程和 Worker 线程内存完全隔离,只能通过postMessage互发消息。

先看主线程怎么创建 Worker 并收发消息:

import{worker,MessageEvents}from'@kit.ArkTS';@Entry@Componentstruct WorkerDemo{@Stateresult:string='等待计算...'privateworkerInstance:worker.ThreadWorker|null=nullaboutToAppear(){this.workerInstance=newworker.ThreadWorker('entry/ets/workers/ComputeWorker.ts')this.workerInstance.onmessage=(e:MessageEvents)=>{this.result=`计算结果:${e.data.result}`}this.workerInstance.onerror=(e:ErrorEvent)=>{console.error(`Worker 出错:${e.message}`)}}startCompute(){if(this.workerInstance){this.workerInstance.postMessage({action:'compute',data:1000})}}aboutToDisappear(){this.workerInstance?.terminate()this.workerInstance=null}build(){Column(){Text(this.result).fontSize(20)Button('开始计算').onClick(()=>this.startCompute())}}}

再看 Worker 线程那边的代码(entry/ets/workers/ComputeWorker.ts):

import{worker,ThreadWorkerGlobalScope,MessageEvents}from'@kit.ArkTS';letworkerPort:ThreadWorkerGlobalScope=worker.workerPort;workerPort.onmessage=(e:MessageEvents)=>{if(e.data.action==='compute'){letnum:number=e.data.dataasnumberletresult:number=0for(leti=0;i<num;i++){result+=i}workerPort.postMessage({result:result})}}workerPort.onerror=(e:ErrorEvent)=>{console.error(`Worker 内部错误:${e.message}`)}

关键代码讲解:

  • new worker.ThreadWorker('entry/ets/workers/ComputeWorker.ts')—— 主线程创建 Worker,参数是 Worker 脚本的相对路径,路径写错直接报错
  • worker.workerPort—— Worker 线程侧的通信端口,必须定义在顶层作用域,不能用const
  • postMessage()—— 发送消息,数据会被序列化后传递
  • onmessage—— 接收消息的回调,e.data就是对方发过来的数据
  • terminate()—— 主线程主动销毁 Worker,组件销毁时一定要调,否则线程泄漏

耗时计算实战

来个真实的场景:图片灰度处理。一张 1920×1080 的图片有 200 万像素,主线程处理直接卡死。

import{worker,MessageEvents}from'@kit.ArkTS';import{image}from'@kit.ImageKit';@Entry@Componentstruct ImageProcessPage{@StatesrcPixelMap:PixelMap|null=null@StateprocessedPixelMap:PixelMap|null=nullprivateworkerInstance:worker.ThreadWorker|null=nullaboutToAppear(){this.workerInstance=newworker.ThreadWorker('entry/ets/workers/GrayWorker.ts')this.workerInstance.onmessage=(e:MessageEvents)=>{if(e.data.type==='progress'){console.info(`处理进度:${e.data.percent}%`)}if(e.data.type==='done'){letbuffer=e.data.bufferasArrayBuffer image.createPixelMap(buffer,{size:{width:e.data.width,height:e.data.height},pixelFormat:4,alphaType:0}).then((pixelMap:PixelMap)=>{this.processedPixelMap=pixelMap})}}}asyncprocessImage(){if(!this.srcPixelMap||!this.workerInstance)returnletimageInfo=awaitthis.srcPixelMap.getImageInfo()letwidth=imageInfo.size.widthletheight=imageInfo.size.heightletbuffer=newArrayBuffer(width*height*4)awaitthis.srcPixelMap.readPixelsToBuffer(buffer)this.workerInstance.postMessage({type:'gray',buffer:buffer,width:width,height:height})}aboutToDisappear(){this.workerInstance?.terminate()this.workerInstance=null}build(){Column(){Row(){Image(this.srcPixelMap).width('45%')Image(this.processedPixelMap).width('45%')}Button('灰度处理').onClick(()=>this.processImage())}}}

Worker 端灰度处理逻辑:

import{worker,ThreadWorkerGlobalScope,MessageEvents}from'@kit.ArkTS';letworkerPort:ThreadWorkerGlobalScope=worker.workerPort;workerPort.onmessage=(e:MessageEvents)=>{if(e.data.type==='gray'){letbuffer=e.data.bufferasArrayBufferletdata=newUint8Array(buffer)letwidth=e.data.widthasnumberletheight=e.data.heightasnumberlettotal=width*heightfor(leti=0;i<total;i++){letoffset=i*4letr=data[offset]letg=data[offset+1]letb=data[offset+2]letgray=Math.floor(0.299*r+0.587*g+0.114*b)data[offset]=gray data[offset+1]=gray data[offset+2]=grayif(i%50000===0){workerPort.postMessage({type:'progress',percent:Math.floor((i/total)*100)})}}workerPort.postMessage({type:'done',buffer:buffer,width:width,height:height})}}

关键代码讲解:

  • readPixelsToBuffer(buffer)—— 把 PixelMap 的像素数据读到 ArrayBuffer,这个 buffer 可以跨线程传递
  • new Uint8Array(buffer)—— Worker 端用 Uint8Array 视图操作像素数据,每 4 字节一组(RGBA)
  • 灰度算法0.299R + 0.587G + 0.114B—— 经典的加权灰度公式,符合人眼对绿色更敏感的特性
  • 中间穿插的progress消息 —— 让主线程知道处理进度,可以显示进度条
  • ArrayBuffer 是可转移对象,传给 Worker 后主线程就不能再访问了,Worker 处理完需要传回来

线程间数据传递(序列化)

Worker 通信基于结构化克隆算法,不是所有数据都能传。来理清楚:

✅ 可以直接传递的:

  • 基本类型:string、number、boolean
  • ArrayBuffer、TypedArray
  • 普通对象(plain object)
  • 数组

❌ 不能直接传递的:

  • 函数
  • 带原型链的对象(class 实例)
  • DOM 对象
  • @State@Observed修饰的复杂类型

如果你需要传 class 实例,手动序列化成普通对象再传:

interfaceUserData{name:stringage:number}classUser{name:string=''age:number=0toJSON():UserData{return{name:this.name,age:this.age}}}letuser=newUser()user.name='鸿蒙'user.age=3this.workerInstance.postMessage(user.toJSON())

线程池管理

单个 Worker 好搞,但如果你需要同时处理多个任务呢?比如图片列表要并发压缩,总不能每张图建一个 Worker 吧。

简单实现一个 Worker 池:

import{worker,MessageEvents}from'@kit.ArkTS';classWorkerPool{privateworkers:worker.ThreadWorker[]=[]privatetaskQueue:{action:string;data:Object;resolve:Function}[]=[]privatebusySet:Set<number>=newSet()privatemaxSize:number=4constructor(){for(leti=0;i<this.maxSize;i++){letw=newworker.ThreadWorker('entry/ets/workers/PoolWorker.ts')letindex=i w.onmessage=(e:MessageEvents)=>{this.busySet.delete(index)this.processQueue()}this.workers.push(w)}}execute(action:string,data:Object):Promise<Object>{returnnewPromise((resolve)=>{this.taskQueue.push({action,data,resolve})this.processQueue()})}privateprocessQueue(){while(this.taskQueue.length>0){letidleIndex=this.findIdleWorker()if(idleIndex===-1)breaklettask=this.taskQueue.shift()!this.busySet.add(idleIndex)this.workers[idleIndex].postMessage({action:task.action,data:task.data})}}privatefindIdleWorker():number{for(leti=0;i<this.workers.length;i++){if(!this.busySet.has(i))returni}return-1}destroy(){this.workers.forEach(w=>w.terminate())this.workers=[]}}

关键代码讲解:

  • maxSize: 4—— 最多 4 个 Worker 并发,别开太多,线程本身有内存开销
  • taskQueue—— 任务队列,Worker 忙不过来时先排队
  • busySet—— 记录哪些 Worker 在忙,processQueue每次找空闲 Worker 分配任务
  • destroy()—— 用完一定要销毁所有 Worker

实际项目中建议用 TaskPool 代替手动线程池,除非你的任务确实需要 Worker 的长驻特性。

写在最后

多线程不是银弹,但主线程卡顿的问题必须解决。记住两个原则:UI 只管渲染,计算扔给子线程;短任务 TaskPool,长任务 Worker。

Worker 用完要terminate(),数据传递注意序列化限制,线程池别开太多——这三点做到,多线程就不会给你惹麻烦。下一篇聊聊 UIAbility 生命周期,跟 Worker 的销毁时机密切相关,建议一起看。

http://www.jsqmd.com/news/1178796/

相关文章:

  • CentOS 7 搭建 DNS 主从服务
  • 我的本地化AI项目(三)
  • Claude 被封号,字节免费提供 GLM-5.2
  • 生产环境Socket编程落地实战指南(TCP/UDP工程化方案)
  • 百度网盘解析工具完整指南:三步告别限速困扰,实现高速下载自由
  • Ubuntu 20.04源码编译ROS2 Foxy实战指南
  • 等价类划分法实战:城市电话号码15个测试用例设计,覆盖4有效11无效
  • Prometheus Pushgateway v1.2.0 部署与配置:Kubernetes 集群 3 步集成实战
  • TPS61170与TM4C129XNCZAD的高效DC-DC升压方案解析
  • 高精度ADC选型与MCP3551应用指南
  • Android Studio完整界面汉化指南:5分钟实现IDE本地化终极方案
  • BepInEx框架解析:解决Unity插件开发三大痛点与实战优化
  • AI FDE 能力模型详解:从价值判断到资产复用的七项能力
  • 无血性者,难以为领
  • 读代码、修 Bug、补测试、写文档:四套拿来即用的工作流模板
  • ArcGIS Pro 3.2 三维地质建模:从钻孔数据到MultiPatch体模型的7步实战
  • 模板驱动型文档自动化:结构化组装替代手工写作
  • Tableau 自定义地理编码指南:解决 3 类无法识别位置数据的实战方案
  • IEEE 754 标准解析:从 32 位 float 到 64 位 double 的精度与范围对比
  • 电脑上的功能键到底都是干啥的?一篇文章教你使用功能键
  • Pixhawk固件加载全流程:从Bootloader刷写到飞行验证
  • ADS 2021 三大仿真模型导入实战:DesignKit/SNP/SPICE 的 3 种场景化应用
  • 从 HydroShear 93% Sim-to-Real 成功率看触觉数据标注工具的技术演进
  • C++实现KD-Tree:从原理到实战,解决多维空间最近邻搜索难题
  • 告别GRC繁琐配置!LabVIEW+Lime SDR让信号采集吞吐量飙升5倍
  • Sqribble:轻量级文档工业化流水线解析
  • Perplexity搜索响应慢、信息冗余、来源模糊?紧急修复的4个实时调优参数(2024 Q2最新API行为分析)
  • Node-gyp v10 环境配置:Windows/macOS/Linux 三平台 Python 3.12 路径设置 3 步法
  • 从零实现C++哈希表:核心原理、冲突解决与性能优化实战
  • 使用WinDbg诊断C++安装程序线程堵塞与安全软件冲突问题