前言
线程是一个操作系统能够运算调度的最小单位,被包含在进程之中,也是进程中的实际运作单位。而在一个应用中(也就是同一个Bundle名)也许会有很多个进程,但是所有的UIAbility、ServiceExtensionAbility和DataShareExtensionAbility最终还是会运行在同一个主进程中,而同一类型的ExtensionAbility(除ServiceExtensionAbility和DataShareExtensionAbility外)均是运行在一个独立进程中。在鸿蒙开发中,每个进程都有一个主线程。
注:WebView拥有独立的渲染进程
Stage模型下的线程类型
在鸿蒙开发的应用进程中,都会有一个主线程(UI线程),对界面进行更新、用户交互响应等操作。对于耗时任务,就需要创建工作线程进行处理,避免阻塞主线程影响UI流畅性。
主线程
1、执行UI绘制,主线程负责处理与用户界面相关的所有操作,包括布局计算、渲染以及屏幕刷新等。在鸿蒙系统中,ArkTs引擎用于管理主线程上的UI渲染。
2、管理主线程的ArkTs引擎实例,使多个UIAbility组件能够运行在其之上。
3、管理其他线程的Arkts引擎实例,例如使用TaskPool(任务池)创建任务或取消任务、启动和终止Worker线程。
4、分发交互事件,主线程接收并发来自用户的触摸事件以及其他系统事件给相应的组件进行处理。
5、消息循环,鸿蒙系统基于消息机制实现线程间的通信和任务调度,主线程维护了一个消息队列,通过循环处理这些消息来响应不同的应用程序事件。
6、处理应用代码的回调,包括事件处理和生命周期管理。
7、接收TaskPool以及Worker线程发送的消息。
线程间通信
在HarmonyOS应用架构中为了保证应用的流畅性和响应性,非UI相关的耗时操作通常不会在主线程上执行,而是需要创建额外的工作线程或任务来完成。
HarmonyOS提供了两种线程间通信的方式,分别是Emitter和Worker。
- Emitter(发射器):Emitter主要适用于线程间的事件同步。它可以在不同的线程之间传递事件,并确保事件的顺序和同步性。通过Emitter,一个线程可以触发一个事件,然后其他线程可以监听并处理这个事件。这有助于不同线程之间的数据共享和协调。
-
Worker(工作者):Worker主要用于新开一个线程执行耗时任务。当需要执行一些耗时操作时,为了不阻塞主任务的执行,可以使用Worker线程。Worker线程是在主线程的上下文中创建的独立线程,它可以执行一些耗时任务,如网络访问、文件读写等。工作线程可以与主线程并行执行,以提高应用的响应性和性能。
Emitter
Emitter主要提供线程间发送和处理事件的能力,包括对持续订阅事件或单次订阅事件的处理、取消订阅事件、发送事件到事件队列等。
开发步骤
1、订阅事件
import emitter from '@ohos.events.emitter';
// 定义一个eventId为1的事件
let event: emitter.InnerEvent = {
eventId: 1
};
// 收到eventId为1的事件后执行该回调
let callback = (eventData: emitter.EventData): void => {
console.info('执行了!!!');
};
// 订阅eventId为1的事件
emitter.on(event, callback);
2、发送事件
import emitter from "@ohos.events.emitter";
// 定义一个eventId为1的事件,事件优先级为Low
let event = {
eventId: 1,
priority: emitter.EventPriority.LOW
};
let eventData = {
data: {
"content": "c",
"id": 1,
"isEmpty": false,
}
};
// 发送eventId为1的事件,事件内容为eventData
emitter.emit(event, eventData);
3、取消事件
import emitter from '@ohos.events.emitter';
// 取消eventID为"eventId"的事件回调处理函数 emitterCallback
// 如果该回调处理函数没有被订阅,则不做任何处理
emitter.off("eventId", () => {
console.info('callback');
});
Worker
Worker主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与主线程分离,在后台线程中运行一个脚本操作耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞主线程的运行。
特点
- 可以和主线程并行
- 创建Worker的线程叫做宿主线程(不一定是主线程)
- Worker中不能直接更新Page
Worker线程的运作机制
Worker的开发步骤
Worker线程文件需要放在“模块名/src/main/ets”目录层级下,否则不会被打包到应用中。
创建Worker的方式
1、手动创建
开发者需要手动创建相关目录及文件:
1.1、鼠标右键模块名->新建->目录,取名叫Worker
1.2、鼠标右键Worker目录->新建->Arkts File文件,取名为Worker.ets
1.3、在工程的模块级build-profile.json5文件的buildOption属性中添加配置信息
1.4、Worker.ets文件中添加功能
import worker from '@ohos.worker';
let parent = worker.workerPort;
// 处理来自主线程的消息
parent.onmessage = function(message) {
console.info("onmessage: " + message)
// 发送消息到主线程
parent.postMessage("message from worker thread.")
}
1.5、主线程中初始化和使用Worker
import worker from '@ohos.worker';
let wk = new worker.ThreadWorker("entry/ets/workers/worker.ets");
// 发送消息到worker线程
wk.postMessage("message from main thread.")
// 处理来自worker线程的消息
wk.onmessage = function(message) {
console.info("message from worker: " + message)
// 根据业务按需停止worker线程
wk.terminate()
}
2、自动创建
右键模块名->新建->Worker,会自动新建一个Worker目录跟Worker.ets文件,并且在build-profile.json5文件的buildOption属性中自动添加配置信息
Worker注意事项
- Worker的创建和销毁会耗费性能,建议合理创建并管理Worker重复使用。
- Worker空闲的时候也会一直运行,当不需要Worker的时候,可以调用terminate()接口或parentPort.close()方法主动销毁Worker。
- 当Worker处于已销毁或销毁中,调用其功能接口会报错。
- Worker数量限制最多同时存在8个Worker。
- 序列化传输的数据量大小限制为16MB。
TaskPool
用于执行耗时操作,支持设置调度优先级、负载均衡等功能。
使用TaskPool(从API9开始支持):
import taskpool from '@ohos.taskpool';
TaskPool(任务池)作用是为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性,并且不需要关心线程实例的生命周期。可以使用任务池API创建后台任务(Task),并对所创建的任务进行如任务执行、任务取消的操作。
可以使用任务池API创建不受限制的任务,但是出于内存因素不建议。此外在任务中执行阻塞操作,特别是无限期阻塞操作(像定时器和延时器),长时间的阻塞操作会占据工作线程,可能会阻塞其他任务调度,影响应用性能。
任务优先级
任务池API接口Priority表示所创建的任务优先级,分为HIGH(高)、MEDIUM(中)、LOW(低)三个优先级,开发者创建的任务默认为MEDIUM优先级,并且创建的同一优先级任务的执行顺序可以由开发者自己决定,任务真实执行的顺序与调用任务池API提供的任务执行接口顺序一致。
名称 | 值 | 说明 |
---|---|---|
HIGH | 0 | 任务为高优先级。 |
MEDIUM | 1 | 任务为中优先级。 |
LOW | 2 | 任务为低优先级。 |
负载均衡机制
负载均衡(Load Balancing)是一种通过将工作负载分布到多个计算资源上,以提高系统性能、增加可用性和容错能力的技术。负载均衡还需要考虑到高可用性、容错能力、安全性等方面的因素,以确保系统的稳定运行和良好性能。
常见的负载均衡机制:
- 基于轮询的负载均衡
- 基于权重的负载均衡
- 基于最小连接数的负载均衡
- 基于响应时间的负载均衡
- 基于内容的负载均衡
在鸿蒙的线程中当同一时间待执行的任务数量大于任务池工作线程数量,任务池会根据负载均衡机制进行扩容,增加工作线程数量,减少整体等待时长。同样,当执行的任务数量减少,工作线程数量大于执行任务数量,部分工作线程处于空闲状态,任务池会根据负载均衡机制进行缩容,减少工作线程数量。
TaskPool运作机制
TaskPool支持开发者在主线程封装任务抛给任务队列,系统选择合适的工作线程,进行任务的分发及执行,再将结果返回给主线程。接口直观易用,支持任务的执行、取消,以及指定优先级的能力,同时通过系统统一线程管理,结合动态调度及负载均衡算法,可以节约系统资源。系统默认会启动一个任务工作线程,当任务较多时会扩容,工作线程数量上限跟当前设备的物理核数相关,具体数量内部管理,保证最优的调度及执行效率,长时间没有任务分发时会缩容,减少工作线程数量。
TaskPool使用注意事项
-
实现任务的函数需要使用装饰器@Concurrent标注,且仅支持在.ets文件中使用。
-
从API version 11开始,实现任务的函数需要使用类方法时,该类必须使用装饰器@Sendable标注,且仅支持在.ets文件中使用。
-
任务函数在TaskPool工作线程的执行耗时不能超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),否则会被强制退出。
-
实现任务的函数入参需满足序列化支持的类型,详情请参见TaskPool和Worker支持的序列化类型。
-
ArrayBuffer参数在TaskPool中默认转移,需要设置转移列表的话可通过接口setTransferList()设置。
-
由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用,具体请见多线程安全注意事项。
-
序列化传输的数据量大小限制为16MB。
-
TaskPool任务只支持引用参数传递或者import的变量,不支持使用闭包变量;只支持普通函数或者async函数,不支持类成员函数或者匿名函数。
使用示例
// 支持普通函数、引用入参传递
@Concurrent
function printArgs(args: string): string {
console.info("func: " + args);
return args;
}
async function taskpoolExecute(): Promise<void> {
// taskpool.execute(task)
let task: taskpool.Task = new taskpool.Task(printArgs, "create task, then execute");
console.info("taskpool.execute(task) result: " + await taskpool.execute(task));
// taskpool.execute(function)
console.info("taskpool.execute(function) result: " + await taskpool.execute(printArgs, "execute task by func"));
}
taskpoolExecute();
通过async函数
@Concurrent
async function delayExcute(): Promise<Object> {
let ret = await Promise.all<Object>([
new Promise<Object>(resolve => setTimeout(resolve, 1000, "resolved"))
]);
return ret;
}
async function taskpoolExecute(): Promise<void> {
taskpool.execute(delayExcute).then((result: Object) => {
console.info("taskPoolTest task result: " + result);
}).catch((err: string) => {
console.error("taskpool test occur error: " + err);
});
}
taskpoolExecute();
TaskPool和Worker对比
TaskPool(任务池)和Worker的作用是为应用程序提供一个多线程的运行环境,用于处理耗时的计算任务或其他密集型任务。可以有效地避免这些任务阻塞主线程,从而最大化系统的利用率,降低整体资源消耗,并提高系统的整体性能。
实现特点对比
实现 | TaskPool | Worker |
---|---|---|
内存模型 | 线程间隔离,内存不共享 | 线程间隔离,内存不共享 |
参数传递机制 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。 支持ArrayBuffer转移和SharedArrayBuffer共享 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。 支持ArrayBuffer转移和SharedArrayBuffer共享 |
参数传递 | 直接传递,无需封装,默认进行transfer | 消息对象唯一参数,需要自己封装 |
方法调用 | 直接将方法传入调用 | 在Worker线程中进行消息解析并调用对应方法 |
返回值 | 异步调用后默认返回 | 主动发送消息,需在onmessage解析赋值 |
生命周期 | TaskPool自行管理生命周期,无需关心任务负载高低。 | 开发者自行管理Worker的数量及生命周期 |
任务池个数上限 | 自动管理,无需配置 | 同个进程下,最多支持同时开启8个Worker线程 |
任务执行时长上限 | 3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时) | 无限制 |
设置任务的优先级 | 支持配置任务优先级 | 不支持 |
执行任务的取消 | 支持取消已经发起的任务 | 不支持 |
线程复用 | 支持 | 不支持 |
任务延时执行 | 支持 | 不支持 |
设置任务依赖关系 | 支持 | 不支持 |
串行队列 | 支持 | 不支持 |
任务组 | 支持 | 不支持 |
适用场景对比
TaskPool和Worker均支持多线程并发能力。由于TaskPool的工作线程会绑定系统的调度优先级,并且支持负载均衡(自动扩缩容),而Worker需要开发者自行创建,存在创建耗时以及不支持设置调度优先级,故在性能方面使用TaskPool会优于Worker,因此大多数场景推荐使用TaskPool。
TaskPool偏向独立任务维度,该任务在线程中执行,无需关注线程的生命周期,超长任务(大于3分钟)会被系统自动回收;而Worker偏向线程的维度,支持长时间占据线程执行,需要主动管理线程生命周期。
常见的一些开发场景及适用具体说明如下:
-
运行时间超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时)的任务。例如后台进行1小时的预测算法训练等CPU密集型任务,需要使用Worker。
-
有关联的一系列同步任务。例如在一些需要创建、使用句柄的场景中,句柄创建每次都是不同的,该句柄需永久保存,保证使用该句柄进行操作,需要使用Worker。
-
需要设置优先级的任务。例如图库直方图绘制场景,后台计算的直方图数据会用于前台界面的显示,影响用户体验,需要高优先级处理,需要使用TaskPool。
-
需要频繁取消的任务。例如图库大图浏览场景,为提升体验,会同时缓存当前图片左右侧各2张图片,往一侧滑动跳到下一张图片时,要取消另一侧的一个缓存任务,需要使用TaskPool。
-
大量或者调度点较分散的任务。例如大型应用的多个模块包含多个耗时任务,不方便使用8个Worker去做负载管理,推荐采用TaskPool。