鸿蒙高性能API设计理念(下)
在实际开发中,尽管同步与异步API提供了基本的并发能力,开发者仍可能出于性能或架构考虑,选择手动创建子线程来处理特定任务,尤其是在业务逻辑复杂或处理密集型任务时。HarmonyOS中的并发、线程和锁机制是开发高性能、稳定应用的关键。并发与并行是任务处理的不同策略,前者通过时间片轮转实现,后者依赖多核设备。线程和锁机制基于共享内存,保障多线程内存一致性,但锁的使用存在挑战,如死锁和代码维护难度。Wo
即便同步与异步API已经提供了基本的并发能力,在实际开发中,开发者仍可能出于性能或架构考虑,选择手动创建子线程来处理特定任务,避免主线程负担过重。尤其在业务逻辑复杂或处理密集型任务时,自定义线程成为必要手段。那么,如何在这种场景下合理地创建和管理子线程呢?
1. 深入理解 HarmonyOS 中的并发、线程和锁机制
在 HarmonyOS 应用开发领域,并发、线程和锁是极为关键的概念,深刻理解它们有助于打造高性能、稳定的应用程序。如下图所示。
1.1. 并发与并行:任务处理的不同策略
1.1.1. 并发
并发指的是在同一时间应对多项任务的能力。在单核设备中,并发任务通过 CPU 时间片轮转的方式实现。比如,假设有任务 1、任务 2、任务 3 和任务 4,在单核 CPU0 上,它们会按顺序轮流占用 CPU 资源执行,看似同时在进行,实则是快速切换执行,给人一种同时处理多项任务的错觉 。
1.1.2. 并行
并行强调的是同一时间真正做多项任务的能力,这依赖于多核设备。在多核环境下,如具有 CPU0、CPU1、CPU2 的设备,不同任务可以被分配到不同的 CPU 核心上同时执行。例如任务 1 在 CPU0 执行,任务 2 在 CPU1 执行,任务 3 在 CPU2 执行,而任务 4 也可在合适的 CPU 核心上并行处理,极大提升了任务处理效率。
1.2. 线程和锁:保障内存一致性的关键
1.2.1. 基于共享内存
线程和锁机制是基于共享内存的,它们更接近于硬件本质。在多线程环境下,为保证多线程内存的一致性,需要使用锁等同步语义。比如当多个线程同时访问和修改共享内存中的数据时,若不加以控制,就会出现数据竞争等问题,导致程序运行结果不可预测。
1.2.2. 锁的优化类型
为提升性能,有多种锁的优化类型。乐观锁假定大概率不会发生冲突,仅在提交更新时检查冲突,适用于读多写少场景;自旋锁在等待锁时不放弃 CPU,而是循环检测锁是否可用,减少线程上下文切换开销;偏向锁则是在无竞争情况下,把锁偏向第一个获得它的线程,减少锁获取的开销;精准内存屏障用于控制内存操作的顺序,确保数据的一致性和可见性。
1.2.3. 使用挑战
虽然锁机制能保障多线程内存一致性,但也存在使用难点。锁的正确使用难度大,容易出现死锁情况,即多个线程相互等待对方释放锁,导致程序无法继续执行。而且,涉及锁的代码难以进行测试和维护,开发人员需要具备较高的技术水平,才能实现性能优秀的多线程程序。
总之,在 HarmonyOS 应用开发中,合理运用并发、线程和锁机制,趋利避害,才能充分发挥系统性能优势,打造出优质的应用体验。
2. 并发API:Worker API
在 HarmonyOS 应用开发领域,为了打造流畅、高效的应用体验,处理好耗时任务与主线程的关系至关重要,Worker API 正是解决这一问题的关键技术。
2.1. 运行机制
2.1.1. 主线程与工作线程的关系
主线程包含基础设施、对象和代码等关键要素。通过序列化机制,可派生出工作线程 1、工作线程 2 等多个工作线程。这些工作线程也拥有各自的基础设施、对象和代码,它们与主线程相互独立,并行运作。
2.1.2. 消息传递与协作
- 主线程操作:在主线程中,首先从
@ohos.worker
模块导入Worker
类。然后通过new Worker('./worker.ets')
创建一个 Worker 实例,这里的./worker.ets
指定了工作线程执行的脚本文件路径。接着,通过worker.onmessage
方法监听来自工作线程的消息,一旦接收到消息,就可进行相应处理。例如,使用worker.postMessage(10)
向工作线程发送消息,要求计算第 10 个斐波那契数。 - 工作线程响应:在工作线程对应的
worker.ets
文件中,先从@ohos.worker
导入相关内容。通过parentPort.onMessage
监听来自主线程的消息,当收到消息后,获取其中的数据(如const num = event.data;
),然后调用fibonacci
函数进行计算。计算完成后,再使用postMessage(result)
将结果发回主线程。其中,fibonacci
函数采用递归方式实现斐波那契数的计算,当n <= 1
时返回n
,否则返回fibonacci(n - 1) + fibonacci(n - 2)
。
2.2. 应用价值与场景
Worker API 让开发者能够轻松实现耗时任务的异步处理,避免主线程阻塞。在实际应用开发中,无论是处理复杂的图形渲染计算、大数据量的解析处理,还是等待网络请求返回数据等场景,都可以借助 Worker API,将这些任务放到后台线程执行,确保应用的主线程能够及时响应用户操作,提升应用的整体性能和用户体验。
2.3. Worker API 示例
// 主线程代码
import worker from '@ohos.worker';
// 创建Worker线程
const fibWorker = new worker.Worker('./fibonacci.worker.js');
// 发送计算请求
fibWorker.postMessage(40);
// 接收计算结果
fibWorker.onmessage = (event) => {
console.info(`斐波那契结果: ${event.data}`);
};
// worker线程代码 (fibonacci.worker.js)
import worker from '@ohos.worker';
// 监听主线程消息
globalThis.onmessage = (event) => {
const result = calculateFibonacci(event.data);
// 返回计算结果
globalThis.postMessage(result);
};
function calculateFibonacci(n) {
if (n <= 1) return n;
return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
}
3. 并发API:TaskPool API
在 HarmonyOS 应用开发的高性能架构设计中,并发编程是提升用户体验的关键技术。除了前文介绍的 Worker API,HarmonyOS 还提供了 TaskPool API 这一强大工具,二者在不同场景下发挥着独特作用。
3.1. TaskPool API 的核心功能
TaskPool,即任务池,其核心作用在于为应用程序构建多线程运行环境。它能够显著降低整体资源的消耗,提升系统的整体性能。更为便利的是,开发者在使用时无需操心线程实例的生命周期管理,这大大简化了开发流程。
3.2. 运行机制剖析
3.2.1. 任务队列与分发
在 TaskPool 的运行架构中,首先存在一个任务队列。任务进入队列后,由任务分发管理器进行处理。任务分发管理器具备优先级调度和负载均衡的能力,能根据系统的统一管理策略,将任务合理分配到不同的任务池工作线程中。
3.2.2. 任务池工作线程
任务池工作线程具有自适应、可伸缩的特性。它包含多个任务工作线程,如任务工作线程 1、任务工作线程 2、任务工作线程 3、任务工作线程 4 等。这些线程能够灵活应对任务负载的变化,自动调整资源分配,确保任务高效执行。
3.3. 开发者友好特性
3.3.1. 易于开发并发任务
- 易用直观,减少代码编写量:TaskPool API 的设计理念注重开发者体验,使用方式简单直观,无需编写大量复杂代码即可实现并发功能。
- 无需关心并发实例生命周期:开发者无需花费精力去处理线程的创建、销毁等生命周期管理操作,降低了开发难度和出错风险。
- 无需关心场景下并发任务负载轻重:无论任务负载是轻是重,TaskPool API 都能自动进行优化调度,开发者无需手动干预。
3.3.2. 统一任务负载资源管理
- 降低系统资源消耗:通过合理的任务调度和线程管理,TaskPool API 能有效避免资源浪费,降低系统整体资源消耗。
- 提升系统整体性能:借助自适应可伸缩的任务池工作线程以及精准的任务分发管理,系统性能得到显著提升,应用响应更加流畅。
3.4. 代码示例解读
以下代码展示了在主线程中使用 taskpool 执行耗时计算(如斐波那契数列的计算)的过程:
import { taskpool } from '@ohos.taskpool';
@concurrent
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
let result = await taskpool.execute(fibonacci, 10);
首先从@ohos.taskpool
导入taskpool
,然后定义一个带有@concurrent
装饰器的fibonacci
函数用于计算斐波那契数。最后通过taskpool.execute
方法执行该函数并传入参数 10,等待计算结果返回。
总之,TaskPool API 作为 HarmonyOS 应用开发中并发处理的重要工具,与 Worker API 相辅相成,为开发者提供了强大且便捷的多线程编程支持,助力打造高性能、低资源消耗的优质应用。
4. TaskPool 和 Worker的对比
在 HarmonyOS 应用开发中,TaskPool 和 Worker 是两种重要的并发处理机制,它们在实现特点和使用场景上存在诸多差异。
4.1. 实现特点对比
实现 |
TaskPool |
Worker |
参数传递 |
直接传递,无需封装,默认进行 transfer |
消息对象唯一参数,需要自己封装 |
方法调用 |
直接将方法传入调用 |
在 Worker 线程中进行消息解析并调用对应方法 |
返回值 |
异步调用后默认返回 |
主动发送消息,需在 onMessage 解析赋值 |
生命周期 |
自行管理生命周期 |
自行管理 Worker 的数量及生命周期 |
任务池个数上限 |
自动管理,无需配置 |
同个进程下,最多支持同时开启 64 个 Worker 线程,实际数量由进程内存决定 |
任务执行时长上限 |
3 分钟,长时任务无执行时长上限 |
无限制 |
设置任务的优先级 |
支持配置任务优先级 |
不支持 |
执行任务的取消 |
支持取消已经发起的任务 |
不支持 |
线程复用 |
支持 |
不支持 |
任务延时执行 |
支持 |
不支持 |
设置任务依赖关系 |
支持 |
不支持 |
串行队列 |
支持 |
不支持 |
任务组 |
支持 |
不支持 |
4.2. 使用场景对比
TaskPool 的工作线程会绑定系统的调度优先级,并且支持负载均衡;Worker 需要开发者自行创建,存在创建耗时以及不支持设置调度优先级的问题,大多数场景推荐使用 TaskPool。
- TaskPool 偏向独立任务维度,无需关注线程的生命周期,超长任务会被系统自动回收;Worker 偏向线程的维度,支持长时间占据线程执行,需要主动管理线程生命周期。
- 运行时间超过 3 分钟的任务需要使用 Worker。
- 有关联的一系列同步任务,例如在一些需要创建、使用句柄的场景中,句柄创建每次都是不同的,该句柄需永久保存,保证使用该句柄进行操作,需要使用 Worker。
- 需要设置优先级的任务需要使用 TaskPool。
- 需要频繁取消的任务需要使用 TaskPool。
- 大量或者调度点较分散的任务推荐采用 TaskPool。
5. Sendable API
5.1. JS对象的跨线程传递问题
在前文中,我们介绍了两种在鸿蒙系统中实现并发的方式:Worker
和 TaskPool
。虽然这两种方式都可以满足多线程处理的基本需求,但它们在性能上仍存在一些不可忽视的问题,尤其是在进行线程间数据传递时。
无论使用 Worker
还是 TaskPool
,都不可避免地涉及到数据的序列化与反序列化。Worker
模式中需要开发者手动处理序列化/反序列化,而 TaskPool
虽然由框架自动完成这一过程,但本质上的开销是一样的,无法避免。
序列化与反序列化会引发两个主要的性能问题:
- 内存开销增加:数据在序列化前后,分别存在于主线程和子线程中,会在内存中形成两份副本。当传输的数据对象较大,且频繁在线程之间传递时,会显著增加内存使用,尤其在内存资源有限的设备上,可能导致应用变慢,甚至被系统终止。
- 时间开销增加:序列化与反序列化本身是耗时操作。随着数据量的增大,这一过程所耗费的时间也会同步增长,主线程和子线程各执行一次,对性能敏感的应用来说,这种开销可能是不可接受的。
除此之外,序列化还存在功能层面的限制:只能传递可被序列化的数据结构。像函数、类实例(尤其是带有方法的类)这类复杂的内存对象,无法通过序列化完整地进行跨线程传递。
那么,如何才能避免这些问题?答案是:使用 Sendable
类来实现高性能、无复制的数据传递机制。
5.2. Sendable的作用
在 HarmonyOS 应用开发的 ArKTS 语言体系里,Sendable 是保障并发场景下数据安全传递与操作的重要概念。
5.2.1. Sendable 协议的核心功能
符合 Sendable 协议的数据具备在 ArKTS 并发实例间传递的能力。默认情况下,这些数据在并发实例间采用实引用传递,即内存中只存在一份对象,不同线程直接引用该对象。这种机制在提高数据传递效率的同时,也带来了潜在风险:当多个并发实例试图同时更新可变的 Sendable 数据时,会引发数据竞争问题,导致程序运行结果不可预测。为解决这一问题,ArKTS 提供了异步锁机制,用于避免不同实例间的数据竞争,保障数据的一致性和操作的正确性。
5.2.2. Sendable 数据的范围界定
基本数据类型
ArKTS 中的基本数据类型,如 boolean(布尔型)、number(数值型)、string(字符串型)、bigint(大整型)、null(空值)、undefined(未定义),天然属于 Sendable 数据范畴。
容器类型数据
ArKTS 语言标准库中定义的容器类型数据,需要显式引入@arkts.collections
模块后,才可作为 Sendable 数据使用。这些容器类型为开发者提供了更丰富的数据组织和管理方式,同时在并发场景下也遵循 Sendable 协议的规则。
异步锁对象
ArKTS 语言标准库中的 AsyncLock 对象,需显式引入@arkts.utils
模块。它在解决并发数据竞争问题中扮演关键角色,配合 Sendable 数据,为多线程操作提供安全保障。
接口与类
- 继承了
ISendable
接口的类型,需显式引入@arkts.lang
。通过实现该接口,自定义类型能够满足 Sendable 协议要求,在并发实例间安全传递。 - 标注
@Sendable
的 class(类),通过这种标注方式,开发者可明确指定自定义类的数据符合 Sendable 协议,从而在并发环境中正确使用。
联合类型数据
元素均为 Sendable 类型的 union type(联合类型)数据,例如string | number
,也属于 Sendable 数据范围。这种联合类型在实际开发中为处理多样化数据提供了便利,同时也保证了在并发场景下的数据安全性。
5.2.3. Sendable 数据在多线程间的传递与操作
在实际应用中,Sendable 数据在主线程和子线程间传递时,是通过引用直接传递的,这属于静态类型无锁化的只读共享模式。这意味着在只读操作时,不会涉及锁的操作,提高了数据访问效率。但当涉及对可变 Sendable 数据的写操作时,就需要借助 ArKTS 的异步锁机制来协调多线程间的操作,防止数据竞争,确保数据的完整性和准确性。
理解 Sendable 的作用、数据范围以及其在多线程环境下的工作机制,是开发者在 HarmonyOS 应用开发中处理并发任务、保障应用性能和数据安全的重要基础。
5.3. 在Worker和TaskPool中使用Sendable
@Sendable装饰器声明并校验Sendable class,@Concurrent装饰器声明该方法是线程安全的。
5.3.1. 在Worder下使用
import { taskpool, worker } from '@kit.ArkTS';
@Sendable
class A {}
let a: A = new A();
@Concurrent
function foo(a: A) {}
let task: taskpool.Task = new taskpool.Task(foo, a);
let w: worker.ThreadWorker = new worker.ThreadWorker("entry/ets/worker");
// 1. TaskPool 共享传输实现方式
taskpool.execute(task).then(() => {})
// 2. Worker 共享传输实现方式
w.postMessageWithSharedSendable(a)
// 3. TaskPool 拷贝传输实现方式
taskpool.setCloneList([a])
taskpool.execute(task).then(() => {})
// 4. Worker 拷贝传输实现方式
w.postMessage(a)
- 导入模块:
import { taskpool, worker } from '@kit.ArkTS';
从@kit.ArkTS
模块中导入taskpool
和worker
,用于使用 TaskPool 和 Worker 相关功能。 - 定义 Sendable 类:
@Sendable class A {}
使用@Sendable
装饰器声明类A
,表示该类的实例数据可在并发实例间传递,let a: A = new A();
创建类A
的实例a
。 - 定义并发函数:
@Concurrent function foo(a: A) {}
使用@Concurrent
装饰器声明函数foo
,表示该函数是线程安全的,它接收类A
的实例作为参数。 - 创建任务和 Worker 实例:
let task: taskpool.Task = new taskpool.Task(foo, a);
创建一个 TaskPool 任务,将函数foo
和实例a
作为参数传入;let w: worker.ThreadWorker = new worker.ThreadWorker("entry/ets/wo");
创建一个 Worker 线程实例,指定其入口脚本路径。 - TaskPool 共享传输:
taskpool.execute(task).then(() => {})
使用 TaskPool 执行任务task
,并在任务完成后执行回调(这里是一个空回调),这是共享传输实现方式,直接传递任务和数据引用。 - Worker 共享传输:
w.postMessageWithSharedSendable(a)
使用 Worker 的postMessageWithSharedSendable
方法,将Sendable
类型的实例a
以共享方式发送到工作线程。 - TaskPool 拷贝传输:
taskpool.setCloneList([a])
将实例a
添加到 TaskPool 的克隆列表,这样在执行任务时会传递数据的拷贝;taskpool.execute(task).then(() => {})
执行任务,此时传递的是a
的拷贝。 - Worker 拷贝传输:
w.postMessage(a)
使用 Worker 的postMessage
方法,将实例a
以拷贝方式发送到工作线程。
5.3.2. 在TaskPool下使用
AsyncLock实现异步锁,允许在锁下执行异步操作。
import { taskpool, worker, ArkTSUtils } from '@kit.ArkTS';
@Sendable
class MySendable {
private key: number;
setKey(v: number) {
this.key = v;
}
}
let sendable: MySendable = new MySendable();
@Concurrent
function setSendableValue(sendable: MySendable) {
const lock = ArkTSUtils.locks.AsyncLock.request('lock0');
lock.lockAsync(() => {
sendable.setKey(0);
}, ArkTSUtils.lock.AsyncLockMode.EXCLUSIVE);
}
let task0: taskpool.Task = new taskpool.Task(setSendableValue, sendable);
let task1: taskpool.Task = new taskpool.Task(setSendableValue, sendable);
taskpool.execute(task0);
taskpool.execute(task1);
- 导入模块:
import { taskpool, worker, ArkTSUtils } from '@kit.ArkTS';
从@kit.ArkTS
模块中导入taskpool
、worker
和ArkTSUtils
,其中ArkTSUtils
用于获取异步锁相关功能。 - 定义 Sendable 类:
@Sendable class MySendable {... }
使用@Sendable
装饰器声明类MySendable
,该类有一个私有成员变量key
和一个设置key
值的方法setKey
,let sendable: MySendable = new MySendable();
创建类MySendable
的实例sendable
。 - 定义并发函数:
@Concurrent function setSendableValue(sendable: MySendable) {... }
使用@Concurrent
装饰器声明函数setSendableValue
,它接收MySendable
实例作为参数。函数内部通过ArkTSUtils.locks.AsyncLock.request('lock0');
获取名为lock0
的异步锁,然后使用lock.lockAsync
方法在异步锁保护下执行sendable.setKey(0);
,这里指定锁模式为ArkTSUtils.lock.AsyncLockMode.EXCLUSIVE
(独占模式)。 - 创建任务并执行:
let task0: taskpool.Task = new taskpool.Task(setSendableValue, sendable);
和let task1: taskpool.Task = new taskpool.Task(setSendableValue, sendable);
创建两个 TaskPool 任务,都使用setSendableValue
函数和sendable
实例作为参数;taskpool.execute(task0);
和taskpool.execute(task1);
依次执行这两个任务,在异步锁的保护下,避免对sendable
实例操作时的数据竞争问题。
--THE END--
更多内容请参见视频教程:《HarmonyOS应用开发实战指南》
更多推荐
所有评论(0)