HarmonyOS应用的UI操作必须在主线程执行(如修改UI控件,更新视图这些操作必须在UI线程中进行),如果主线程出现阻塞,那么UI界面就会出现明显的卡顿。因此为了解决此类问题,我们需要将一些耗时的操作例如加载网络数据、查询本地文件、数据等放到子线程中,以提升应用的响应速度和性能。
多线程能力介绍
进程与线程
在单线程中执行的代码都是串行的,即按顺序执行,直到执行完成后,程序才会退出。当程序需要执行多个任务时,每个任务必须等待前一个任务执行完成后才能继续执行,这使得程序的性能非常低下。多线程技术通过使用多个线程来充分利用CPU资源,同时执行多个任务,从而提高程序执行的效率。每个线程都是相互独立的,并能够单独执行、暂停、继续和停止。进程(Process):是操作系统进行资源分配的最小单元。线程(Thread):是操作系统进行运算调度的最小单元,它被包含在进程之中,是进程中的实际运作单位。
多线程的使用场景
多线程的应用场景包括但不限于以下场景:
-
CPU密集型:数据处理、图像处理。当需要大量的数据处理时,可以使用多线程,以提高处理效率
-
I/O密集型:文件读写、网络请求。当需要发起大量的I/O请求时,可以使用多线程,以避免卡主线
-
后台任务:自动化作业处理,比如需要定期完成特定任务。
ArkTS的多线程解决方案
在HarmonyOS的ArkTS侧为多线程提供了两种方式:TaskPool和Worker,应用可以结合自身业务诉求,选择对应的实现方案。
TaskPool简介
任务池(TaskPool)作用是为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性能,且开发者无需关心线程实例的生命周期。TaskPool提供了多种不同的任务能力:
能力 | 任务构建 | 任务执行 | 场景描述 |
---|---|---|---|
普通任务 | new taskpool.Task() | taskpool.execute() | 立即执行的短时任务,耗时不能超过3分钟。 |
延时任务 | new taskpool.Task() | taskpool.executeDelayed() | 为了不影响应用启动的性能,一些不影响启动的初始化类任务往往期望放在延时任务中执行,如拉取线上的配置信息等。 |
长时任务 | new taskpool.LongTask() | taskpool.execute() | 希望长时运行的任务一直保持执行,已为其他模块提供特定的服务,比如日志埋点,后台长链接保活等。 |
串行任务 | new taskpool.SequenceRunner(),new taskpool.Task() | SequenceRunner. execute() | 用于执行一组需要串行执行的任务 |
依赖任务 | task1.addDependency(task2), task1.removeDependency(task2) | taskpool.execute() | 任务之间存在先后依赖关系 |
任务的优先级设置 | 待支持 | 待支持 | 在应用运行时,期望可以给设置空闲时任务,当应用处于闲时(比如冷启动完成后停留在首页)做一些相应的预加载动作(如预加载其他页面),从而提升应用整体性能。 |
TaskPool注意事项
-
实现任务的函数需要使用装饰器@Concurrent标注,且仅支持在.ets文件中使用。
-
由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用,具体请见多线程安全注意事项。
-
序列化传输的数据量大小限制为16MB。
推荐使用场景
TaskPool的工作线程会绑定系统的调度优先级,并且支持负载均衡(自动扩缩容)
-
需要设置优先级的任务。
-
大量或者调度点较分散的任务。
-
需要频繁取消的任务。
TaskPool线程池扩容策略
总体算法TaskPool需要的工作线程数由任务平均执行时间和当前任务数共同决定。任务池会根据过往的执行数据计算出一个预期线程数,但是各个任务的执行时间并不能准确衡量TaskPool当前的负载,因此需要通过任务数来反映。
扩容机制TaskWorker线程创建有一定耗时。为了优化启动阶段的性能和加快任务执行,TaskPool默认创建和预留了一个线程。结合JS的async/await机制和TaskPool的线程复用特性,当任务较少时,一个线程能够正常处理所有任务,此时并不一定会触发扩容机制。当首次执行且任务执行耗时较长的时候,上述算法的平均执行时间并不能立刻得出,因此有新的任务时,将会额外新建两个线程,避免线程池拥塞。当任务执行完成后,仍会采用上述算法。
正常流程下,每当开发者向任务池中抛任务时,都会触发一次扩容检测。扩容检测首先会判断当前的空闲线程数是否大于任务数,若大于,则说明线程池存在空闲线程,不需要扩容即可执行完新的任务。否则通过算法判断需要的线程数进而创建线程到指定数目。
缩容机制当任务耗时且较多时,TaskPool将会新建多个TaskWorker线程。但在空闲时仍保留这么多线程将会导致资源浪费和内存无法下降。因此TaskPool使用了定时器,定时检测TaskPool的负载,负载计算方式仍然采用上述算法。但是考虑到频繁创建和销毁带来的开销,缩容并不会立刻缩到计算出的数目,而是整体沿用了急涨缓停的思想,即立刻扩容到指定数目,但在缩容阶段采用阶梯式下降的方式。空闲时每个检测阶段会尝试释放2个线程(当前策略)。由于涉及到资源释放,需要确保不会因为错误的释放带来野指针等crash行为,缩容阶段仍会去检测当前空闲线程是否可以释放。仅有满足条件的线程才能够被释放。
Worker简介
Worker主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与主线程分离,在后台线程中运行一个脚本操作耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞主线程的运行。
Worker注意事项
-
Worker的创建和销毁耗费性能,建议开发者合理管理已创建的Worker并重复使用。
-
Worker存在数量限制,支持最多同时存在64个Worker。当Worker数量或者内存超出限制时,会抛出相应错误。
推荐的使用场景
- 常驻后台的线程任务
TaskPool与Worker对比
本节将从实现特点和适用场景两个方面来进行TaskPool与Worker的比较。更详细内容可以参考指南TaskPool和Worker的对比
实现 | TaskPool | Worker |
---|---|---|
内存模型 | 线程间隔离,内存不共享。 | 线程间隔离,内存不共享。 |
参数传递机制 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移、SharedArrayBuffer共享、sendable共享。 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 |
参数传递 | 直接传递,无需封装,默认进行transfer。 | 消息对象唯一参数,需要自己封装。 |
方法调用 | 直接将方法传入调用。 | |
在Worker线程中进行消息解析并调用对应方法。 | ||
返回值 | 异步调用后默认返回。 | 主动发送消息,需在onmessage解析赋值。 |
生命周期 | TaskPool自行管理生命周期,无需关心任务负载高低。 | 开发者自行管理Worker的数量及生命周期。 |
任务池个数上限 | 自动管理,无需配置。 | 同个进程下,最多支持同时开启64个Worker线程,实际数量由进程内存决定。 |
任务执行时长上限 | 3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),长时任务无执行时长上限。 | 无限制。 |
设置任务的优先级 | 支持配置任务优先级。 | 不支持。 |
执行任务的取消 | 支持取消已经发起的任务。 | 不支持。 |
线程复用 | 支持。 | 不支持。 |
任务延时执行 | 支持。 | 不支持。 |
设置任务依赖关系 | 支持。 | 不支持。 |
串行队列 | 支持。 | 不支持。 |
任务组 | 支持。 | 不支持。 |
多线程开发常见场景&解决方案
创建&停止线程
TaskPool线程的创建&停止
通过taskpool.Task或者taskpool.LongTask构建TaskPool线程池任务,构造方法如let task: taskpool.Task = new taskpool.Task(taskName, taskFun, args);。
-
taskName为任务名称,此任务名无法在子线程中获取,如果子线程需要使用任务名,则需要将任务名作为参数传递给子线程。
-
taskFun为要执行的逻辑函数,该函数必须使用@Concurrent装饰器装饰。
-
args为任务执行函数的入参。默认值为undefined。
import taskpool from '@ohos.taskpool';
@Concurrent
function add(num1: number, num2: number): number {
return num1 + num2;
}
async function ConcurrentFunc(): Promise<void> {
try {
let task: taskpool.Task = new taskpool.Task(add, 1, 2);
console.info("taskpool res is: " + await taskpool.execute(task));
} catch (e) {
console.error("taskpool execute error is: " + e);
}
}
@Entry
@Component
struct Index {
build() {
Row() {
Column() {
Text('Do Something in Taskpool')
.onClick(() => {
ConcurrentFunc();
})
}
}
}
}
TaskPool任务销毁:对于长时任务(LongTask),除了通过启动外,开发者还需要在任务完成后调用terminateTask方法终止此任务,系统不会主动回收此任务。
let longTask: taskpool.LongTask = new taskpool.LongTask(longTask, 1000); // 1000: sleep time
taskpool.execute(longTask).then((res: Object)=>{
taskpool.terminateTask(longTask);
});
Worker线程的创建&停止
通过new worker.ThreadWorker(‘entry/ets/workers/MyWorker.ets’)的方式加载Worker。HAP中的Worker加载,路径规则为:{moduleName}/ets/{relativePath}。
import worker from '@ohos.worker';
const workerThreadHAP: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets');
HSP中的Worker加载,路径规则为:{moduleName}/ets/{relativePath}。
import worker from '@ohos.worker';
const workerThreadHSP: worker.ThreadWorker = new worker.ThreadWorker('hsp/ets/workers/worker.ets');
HAR中的Worker加载,路径规则为:@{moduleName}/ets/{relativePath}。
import worker from '@ohos.worker';
const workerThreadHAR: worker.ThreadWorker = new worker.ThreadWorker('@har/ets/workers/worker.ets');
Worker线程销毁
Worker的生命周期需要开发者自行进行管理,也就是说创建出Worker线程后,开发者需要管理Worker线程实例,因此当不需要再使用该线程后,需要显示调用Worker的terminate()方法,将Worker线程实例销毁掉。
const workerInstance = new worker.ThreadWorker("entry/ets/workers/worker.ets");
workerInstance.terminate();
线程间通信数据类型说明
此处只做简单说明,具体规格参考相关文档
Sendable类型共享数据类型
Sendable协议定义了ArkTS的可共享对象体系及其规格约束。符合Sendable协议的数据(以下简称 Sendable 数据)可以在ArkTS并发实例间传递。默认情况下,Sendable数据在ArkTS并发实例(主线程、TaskPool、Worker)间通过引用传递。同时,ArkTS也支持Sendable数据在ArkTS并发实例间的拷贝传。
Sendable对象可以支持的属性类型:
-
属性限制:包含基础类型(boolean, number,string,bigint,null,undefined)、其他共享对象、枚举、@arkts.collections下的容器。
-
方法限制:可以传递共享对象中的方法。
当前Sendable对象使用限制较多,如:
-
Sendable class不能使用除了@Sendable的其他装饰器
-
不能使用字面量初始化Sendable类型
-
非Sendable类型不可以as成Sendable类型
更多的规则可参考 Sendable使用规则
Sendable对象的使用:
-
taskpool中使用Sendable对象构建Task时传递参数是,通过args参数将Sendable对象的引用传递给子线程new taskpool.Task(taskName, taskFun, args)。
-
worker中使用Sendable对象通过workerPort.postMessageWithSharedSendable()方法,将Sendable对象的引用传递给子线程。
普通数据类型
普通对象传输采用标准的结构化克隆算法(Structured Clone)进行序列化,此算法可以通过递归的方式拷贝传输对象,相较于其他序列化的算法,支持的对象类型更加丰富。
序列化支持的类型包括:除Symbol之外的基础类型、Date、String、RegExp、Array、Map、Set、Object(仅限简单对象,比如通过“{}”或者“new Object”创建,普通对象仅支持传递属性,不支持传递其原型及方法)、ArrayBuffer、TypedArray。
可转移对象
可转移对象(Transferable object)传输采用地址转移进行序列化,不需要内容拷贝,会将ArrayBuffer的所有权转移给接收该ArrayBuffer的线程,转移后该ArrayBuffer在发送它的线程中变为不可用,不允许再访问。
可共享对象
共享对象SharedArrayBuffer,拥有固定长度,可以存储任何类型的数据,包括数字、字符串等。
共享对象传输指SharedArrayBuffer支持在多线程之间传递,传递之后的SharedArrayBuffer对象和原始的SharedArrayBuffer对象可以指向同一块内存,进而达到内存共享的目的。
SharedArrayBuffer对象存储的数据在同时被修改时,需要通过原子操作保证其同步性,即下个操作开始之前务必需要等到上个操作已经结束。
Native绑定对象
当前支持序列化传输的Native绑定对象主要包含:Context、RemoteObject和PixelMap。
鸿蒙全栈开发全新学习指南
有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以要有一份实用的鸿蒙(HarmonyOS NEXT)学习路线与学习文档用来跟着学习是非常有必要的。
针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。
本路线共分为四个阶段:
第一阶段:鸿蒙初中级开发必备技能
第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH
第三阶段:应用开发中高级就业技术
第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH
《鸿蒙 (Harmony OS)开发学习手册》(共计892页)
如何快速入门?
1.基本概念
2.构建第一个ArkTS应用
3.……
开发基础知识:gitee.com/MNxiaona/733GH
1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……
基于ArkTS 开发
1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……
鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH
鸿蒙入门教学视频:
美团APP实战开发教学:gitee.com/MNxiaona/733GH
写在最后
- 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
- 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
- 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
- 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:
gitee.com/MNxiaona/733GH