ArkTS多线程的HarmonyOS 鸿蒙Next多线程系列(一):ArkTS多线能力入门

发布于 1周前 作者 eggper 来自 鸿蒙OS

ArkTS多线程的HarmonyOS 鸿蒙Next多线程系列(一):ArkTS多线能力入门

HarmonyOS应用的UI操作必须在主线程执行(如修改UI控件,更新视图这些操作必须在UI线程中进行),如果主线程出现阻塞,那么UI界面就会出现明显的卡顿。因此为了解决此类问题,我们需要将一些耗时的操作例如加载网络数据、查询本地文件、数据等放到子线程中,以提升应用的响应速度和性能。

多线程能力介绍

进程与线程

在单线程中执行的代码都是串行的,即按顺序执行,直到执行完成后,程序才会退出。当程序需要执行多个任务时,每个任务必须等待前一个任务执行完成后才能继续执行,这使得程序的性能非常低下。多线程技术通过使用多个线程来充分利用CPU资源,同时执行多个任务,从而提高程序执行的效率。每个线程都是相互独立的,并能够单独执行、暂停、继续和停止。进程(Process):是操作系统进行资源分配的最小单元。线程(Thread):是操作系统进行运算调度的最小单元,它被包含在进程之中,是进程中的实际运作单位。

1_zh-cn_image_0000001902122574.png

多线程的使用场景

多线程的应用场景包括但不限于以下场景:

  1. CPU密集型:数据处理、图像处理。当需要大量的数据处理时,可以使用多线程,以提高处理效率

  2. I/O密集型:文件读写、网络请求。当需要发起大量的I/O请求时,可以使用多线程,以避免卡主线

  3. 后台任务:自动化作业处理,比如需要定期完成特定任务。

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();

})

}

}

}

}<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>

TaskPool任务销毁:对于长时任务(LongTask),除了通过启动外,开发者还需要在任务完成后调用terminateTask方法终止此任务,系统不会主动回收此任务。

let longTask: taskpool.LongTask = new taskpool.LongTask(longTask, 1000); // 1000: sleep time

taskpool.execute(longTask).then((res: Object)=>{

taskpool.terminateTask(longTask);

});<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>

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’);<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>

HSP中的Worker加载,路径规则为:{moduleName}/ets/{relativePath}。

import worker from ‘@ohos.worker’;

const workerThreadHSP: worker.ThreadWorker = new worker.ThreadWorker(‘hsp/ets/workers/worker.ets’);<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>

HAR中的Worker加载,路径规则为:@{moduleName}/ets/{relativePath}。

import worker from ‘@ohos.worker’;

const workerThreadHAR: worker.ThreadWorker = new worker.ThreadWorker(’@har/ets/workers/worker.ets’);<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>

Worker线程销毁

Worker的生命周期需要开发者自行进行管理,也就是说创建出Worker线程后,开发者需要管理Worker线程实例,因此当不需要再使用该线程后,需要显示调用Worker的terminate()方法,将Worker线程实例销毁掉。

const workerInstance = new worker.ThreadWorker(“entry/ets/workers/worker.ets”);

workerInstance.terminate();<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 8px; right: 8px; font-size: 14px;">复制</button>

线程间通信数据类型说明

此处只做简单说明,具体规格参考相关文档

Sendable类型共享数据类型

Sendable协议定义了ArkTS的可共享对象体系及其规格约束。符合Sendable协议的数据(以下简称 Sendable 数据)可以在ArkTS并发实例间传递。默认情况下,Sendable数据在ArkTS并发实例(主线程、TaskPool、Worker)间通过引用传递。同时,ArkTS也支持Sendable数据在ArkTS并发实例间的拷贝传。

Sendable对象可以支持的属性类型:

  1. 属性限制:包含基础类型(boolean, number,string,bigint,null,undefined)、其他共享对象、枚举、@arkts.collections下的容器。

  2. 方法限制:可以传递共享对象中的方法。

当前Sendable对象使用限制较多,如:

  1. Sendable class不能使用除了@Sendable的其他装饰器

  2. 不能使用字面量初始化Sendable类型

  3. 非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。

4 回复
这里是不是少了sendablecke_246.png
好文,感谢分享。

ArkTS在HarmonyOS鸿蒙Next中支持多线程编程,主要通过TaskPool和Worker两种方式实现。TaskPool为应用提供多线程运行环境,支持多种任务类型如普通、延时、长时和串行任务,提升系统性能。Worker用于在后台线程执行耗时操作,避免阻塞主线程。两者均有助于提升应用响应速度和整体性能。如果问题依旧没法解决请加我微信,我的微信是itying888。

回到顶部