HarmonyOS中的多线程并发机制
多线程并发1. 多线程并发概述
并发模型是用来实现不同应用场景中并发任务的编程模型,常见的并发模型分为基于内存共享的并发模型和基于消息通信的并发模型。
Actor并发模型作为基于消息通信并发模型的典范代表,不需要开发者去面对锁带来的一系列复杂偶发的问题,同时并发度也相对较高,因此得到了广泛的支持和利用。
当前ArkTS提供了TaskPool和Worker两种并发能力,TaskPool和Worker都基于Actor并发模型实现。
2 多线程并发模型
内存共享并发模型指多线程同时实行任务,这些线程依靠同一内存并且都有权限访问,线程访问内存前需要抢占并锁定内存的利用劝,没有抢占到内存的线程需要等待其他线程开释利用权再实行。
Actor并发模型每一个线程都是一个独立Actor,每个Actor有自己独立的内存,Actor之间通过消息通报机制触发对方Actor行为,不同Actor之间不能直接访问对方的内存空间。
由于Actor并发模型线程之间不共享内存,需要通过线程间通信机制传输并发任务和任务结果。
内存共享模型
内存共享模型指多线程同时实行任务,这些线程依靠同一内存并且都有权限访问,线程访问内存前需要抢占并锁定内存的利用权,没有抢占到内存的线程需要等待其他线程开释利用权再实行。同一时间只能有一个生产者或斲丧者访问该容器,也就是不同生产者和斲丧者争取利用容器的锁。当一个脚色获取锁之后其他脚色需要等待该脚色开释锁之后才气重新实验获取锁以访问该容器。
https://i-blog.csdnimg.cn/img_convert/3174bce82836870438888aec89794701.png
Actor模型
Actor模型不同脚色之间并不共享内存,生产者线程和UI线程都有自己的假造机实例,两个假造机实例之间拥有独占的内存,相互隔离。生产者生产出结果后通过序列化通信将结果发送给UI线程,UI线程斲丧结果后再发送新的生产任务给生产者线程。
Actor并发模型对比内存共享并发模型的优势在于不同线程间内存隔离,不会产生不同线程竞争同一内存资源的问题。开发者不需要思量对内存上锁导致的一系列功能、性能问题,提升了开发效率。
https://i-blog.csdnimg.cn/img_convert/282ae3fb5b709fbb0fb3261465c2ee11.png
3 TaskPool简介
任务池(TaskPool)作用是为应用程序提供一个多线程的运行情况,降低团体资源的斲丧、进步系统的团体性能,且您无需关心线程实例的生命周期。
TaskPook运作机制
https://i-blog.csdnimg.cn/img_convert/8ea75d40e8a250d21030fe6f738f221f.png
TaskPool支持开发者在宿主线程封装任务抛给任务队列,系统选择符合的工作线程,举行任务的分发及实行,再将结果返回给宿主线程。接口直观易用,支持任务的实行、取消,以及指定优先级的能力,同时通过系统统一线程管理,联合动态调度及负载均衡算法,可以节省系统资源。系统默认会启动一个任务工作线程,当任务较多时会扩容,工作线程数量上限跟当前装备的物理核数相关,具体数量内部管理,保证最优的调度及实行效率,长时间没有任务分发时会缩容,减少工作线程数量。
TaskPool留意事项
[*]实现任务的函数需要利用@Concurrent装饰器标注,且仅支持在.ets文件中利用。
[*]从API version 11开始,跨并发实例通报带方法的实例对象时,该类必须利用装饰器@Sendable装饰器标注,且仅支持在.ets文件中利用。
[*]任务函数在TaskPool工作线程的实行耗时不能超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),否则会被强制退出。
[*]实现任务的函数入参需满意序列化支持的类型
[*]ArrayBuffer参数在TaskPool中默认转移,需要设置转移列表的话可通过接口setTransferList()设置。
[*]由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能利用线程安全的库,例如UI相关的非线程安全库不能利用。
[*]序列化传输的数据量巨细限制为16MB。
[*]Priority的IDLE优先级是用来标记需要在后台运行的耗时任务(例如数据同步、备份),它的优先级别是最低的。这种优先级标记的任务只会在所有线程都空闲的情况下触发实行,并且只会占用一个线程来实行。
[*]Promise不支持跨线程通报,如果TaskPool返回pending或rejected状态的Promise,会返回失败;对于fulfilled状态的Promise,TaskPool会解析返回的结果,如果结果可以跨线程通报,则返回成功。
[*]不支持在TaskPool工作线程中利用AppStorage。
[*]TaskPool支持开发者在宿主线程封装任务抛给任务队列,理论上可以支持任意多的任务,但任务的实行受限于任务的优先级以及系统资源的影响,在工作线程扩容到最大后,可能会导致任务的实行效率下降。
说明
由于@Concurrent标记的函数不能访问闭包,因此@Concurrent标记的函数内部不能调用当前文件的其他函数,例如:
function bar() {
}
@Concurrent
function foo() {
bar(); // 违反闭包原则,报错
}
4 Worker简介
Worker紧张作用是为应用程序提供一个多线程的运行情况,可满意应用程序在实行过程中与宿主线程分离,在后台线程中运行一个脚本举行耗时操作,极大避免类似于盘算密集型或高耽误的任务阻塞宿主线程的运行。
Worker运作机制
https://i-blog.csdnimg.cn/img_convert/7860c13ce600941e29ea4fe7c4f8736e.png
创建Worker的线程称为宿主线程(不一定是主线程,工作线程也支持创建Worker子线程),Worker自身的线程称为Worker子线程(或Actor线程、工作线程)。每个Worker子线程与宿主线程拥有独立的实例,包含基础设施、对象、代码段等,因此每个Worker启动存在一定的内存开销,需要限制Worker的子线程数量。Worker子线程和宿主线程之间的通信是基于消息通报的,Worker通过序列化机制与宿主线程之间相互通信,完成命令及数据交互。
4.1 Woker留意事项
[*]创建Worker时,有手动和自动两种创建方式,手动创建Worker线程目录及文件时,还需同步举行相关设置,。
[*]利用Worker能力时,构造函数中传入的Worker线程文件的路径在不同版本有不同的规则。
[*]Worker创建后需要手动管理生命周期,且最多同时运行的Worker子线程数量为64个。
[*]由于不同线程中上下文对象是不同的,因此Worker线程只能利用线程安全的库,例如UI相关的非线程安全库不能利用。
[*]序列化传输的数据量巨细限制为16MB。
[*]利用Worker模块时,需要在宿主线程中注册onerror接口,否则当Worker线程出现异常时会发生jscrash问题。
[*]不支持跨HAP利用Worker线程文件。
[*]引用HAR/HSP前,需要先设置对HAR/HSP的依靠。
[*]不支持在Worker工作线程中利用AppStorage。
创建Worker的留意事项
Worker线程文件需要放在"{moduleName}/src/main/ets/"目录层级之下,否则不会被打包到应用中。有手动和自动两种创建Worker线程目录及文件的方式。
[*] 手动创建:开发者手动创建相关目录及文件,此时需要设置build-profile.json5的相关字段信息,Worker线程文件才气确保被打包到应用中。
Stage模型:
"buildOption": {
"sourceOption": {
"workers": [
"./src/main/ets/workers/worker.ets"
]
}
}
FA模型:
"buildOption": {
"sourceOption": {
"workers": [
"./src/main/ets/MainAbility/workers/worker.ets"
]
}
}
[*] 自动创建:DevEco Studio支持一键生成Worker,在对应的{moduleName}目录下任意位置,点击鼠标右键 > New > Worker,即可自动生成Worker的模板文件及设置信息,无需再手动在build-profile.json5中举行相关设置。
文件路径留意事项
当利用Worker模块具体功能时,均需先构造Worker实例对象,其构造函数与API版本相关,且构造函数需要传入Worker线程文件的路径(scriptURL)。
// 导入模块
import { worker } from '@kit.ArkTS';
// API 9及之后版本使用:
const worker1: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets');
// API 8及之前版本使用:
const worker2: worker.Worker = new worker.Worker('entry/ets/workers/worker.ets');
Stage模型下的文件路径规则
构造函数中的scriptURL要求如下:
[*]scriptURL的组成包含 {moduleName}/ets 和相对路径 relativePath。
[*]relativePath是Worker线程文件相对于"{moduleName}/src/main/ets/"目录的相对路径。
1) 加载Ability中Worker线程文件场景
加载Ability中的worker线程文件,加载路径规则:{moduleName}/ets/{relativePath}。
import { worker } from '@kit.ArkTS';
// worker线程文件所在路径:"entry/src/main/ets/workers/worker.ets"
const workerStage1: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets');
// worker线程文件所在路径:"testworkers/src/main/ets/ThreadFile/workers/worker.ets"
const workerStage2: worker.ThreadWorker = new worker.ThreadWorker('testworkers/ets/ThreadFile/workers/worker.ets');
2) 加载HSP中Worker线程文件场景
加载HSP中worker线程文件,加载路径规则:{moduleName}/ets/{relativePath}。
import { worker } from '@kit.ArkTS';
// worker线程文件所在路径: "hsp/src/main/ets/workers/worker.ets"
const workerStage3: worker.ThreadWorker = new worker.ThreadWorker('hsp/ets/workers/worker.ets');
3) 加载HAR中Worker线程文件场景
加载HAR中worker线程文件存在以下两种情况:
[*]@标识路径加载形式:所有种类的模块加载本地HAR中的Worker线程文件,加载路径规则:@{moduleName}/ets/{relativePath}。
[*]相对路径加载形式:本地HAR加载该包内的Worker线程文件,加载路径规则:创建Worker对象地点文件与Worker线程文件的相对路径。
说明
当开启useNormalizedOHMUrl(即将工程目录中与entry同级别的应用级build-profile.json5文件中strictMode属性的useNormalizedOHMUrl字段设置为true)或HAR包会被打包成三方包利用时,则HAR包中利用Worker仅支持通过相对路径的加载形式创建。
import { worker } from '@kit.ArkTS';
// @标识路径加载形式:
// worker线程文件所在路径: "har/src/main/ets/workers/worker.ets"
const workerStage4: worker.ThreadWorker = new worker.ThreadWorker('@har/ets/workers/worker.ets');
// 相对路径加载形式:
// worker线程文件所在路径: "har/src/main/ets/workers/worker.ets"
// 创建Worker对象的文件所在路径:"har/src/main/ets/components/mainpage/MainPage.ets"
const workerStage5: worker.ThreadWorker = new worker.ThreadWorker('../../workers/worker.ets');
FA模型下的文件路径规则
构造函数中的scriptURL为:Worker线程文件与"{moduleName}/src/main/ets/MainAbility"的相对路径。
import { worker } from '@kit.ArkTS';
// 主要说明以下三种场景:
// 场景1: Worker线程文件所在路径:"{moduleName}/src/main/ets/MainAbility/workers/worker.ets"
const workerFA1: worker.ThreadWorker = new worker.ThreadWorker("workers/worker.ets", {name:"first worker in FA model"});
// 场景2: Worker线程文件所在路径:"{moduleName}/src/main/ets/workers/worker.ets"
const workerFA2: worker.ThreadWorker = new worker.ThreadWorker("../workers/worker.ets");
// 场景3: Worker线程文件所在路径:"{moduleName}/src/main/ets/MainAbility/ThreadFile/workers/worker.ets"
const workerFA3: worker.ThreadWorker = new worker.ThreadWorker("ThreadFile/workers/worker.ets");
生命周期留意事项
[*]Worker的创建和销毁淹灭性能,建议开发者公道管理已创建的Worker并重复利用。Worker空闲时也会不绝运行,因此当不需要Worker时,可以调用terminate()]接口或close()方法主动销毁Worker。若Worker处于已销毁或正在销毁等非运行状态时,调用其功能接口,会抛出相应的错误。
[*]Worker的数量由内存管理计谋决定,设定的内存阈值为1.5GB和装备物理内存的60%中的较小者。在内存答应的情况下,系统最多可以同时运行64个Worker。如果实验创建的Worker数量超出这一上限,系统将抛出错误:“Worker initialization failure, the number of workers exceeds the maximum.”。实际运行的Worker数量会根据当前内存利用情况动态调整。一旦所有Worker和主线程的累积内存占用超过了设定的阈值,系统将触发内存溢出(OOM)错误,导致应用程序瓦解。
4.2 Woker根本用法示例
[*] DevEco Studio支持一键生成Worker,在对应的{moduleName}目录下任意位置,点击鼠标右键 > New > Worker,即可自动生成Worker的模板文件及设置信息。本文以创建“worker”为例。
[*] 导入Worker模块。
// Index.ets
import { ErrorEvent, MessageEvents, worker } from '@kit.ArkTS'
[*] 在宿主线程中通过调用ThreadWorker的constructor()方法创建Worker对象,当前线程为宿主线程,并注册回调函数。
// Index.ets
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
// 创建Worker对象
let workerInstance = new worker.ThreadWorker('entry/ets/workers/worker.ets');
// 注册onmessage回调,当宿主线程接收到来自其创建的Worker通过workerPort.postMessage接口发送的消息时被调用,在宿主线程执行
workerInstance.onmessage = (e: MessageEvents) => {
let data: string = e.data;
console.info("workerInstance onmessage is: ", data);
}
// 注册onerror回调,当Worker在执行过程中发生异常时被调用,在宿主线程执行
workerInstance.onerror = (err: ErrorEvent) => {
console.info("workerInstance onerror message is: " + err.message);
}
// 注册onmessageerror回调,当Worker对象接收到一条无法被序列化的消息时被调用,在宿主线程执行
workerInstance.onmessageerror = () => {
console.info('workerInstance onmessageerror');
}
// 注册onexit回调,当Worker销毁时被调用,在宿主线程执行
workerInstance.onexit = (e: number) => {
// 当Worker正常退出时code为0,异常退出时code为1
console.info("workerInstance onexit code is: ", e);
}
// 向Worker线程发送消息
workerInstance.postMessage('1');
})
}
.height('100%')
.width('100%')
}
}
[*] 在Worker文件中注册回调函数。
// worker.ets
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
// 注册onmessage回调,当Worker线程收到来自其宿主线程通过postMessage接口发送的消息时被调用,在Worker线程执行
workerPort.onmessage = (e: MessageEvents) => {
let data: string = e.data;
console.info('workerPort onmessage is: ', data);
// 向主线程发送消息
workerPort.postMessage('2');
}
// 注册onmessageerror回调,当Worker对象接收到一条无法被序列化的消息时被调用,在Worker线程执行
workerPort.onmessageerror = () => {
console.info('workerPort onmessageerror');
}
// 注册onerror回调,当Worker在执行过程中发生异常被调用,在Worker线程执行
workerPort.onerror = (err: ErrorEvent) => {
console.info('workerPort onerror err is: ', err.message);
}
5. TaskPool和Worker的对比
TaskPool(任务池)和Worker的作用是为应用程序提供一个多线程的运行情况,用于处置惩罚耗时的盘算任务或其他密集型任务。可以有用地避免这些任务阻塞宿主线程,从而最大化系统的利用率,降低团体资源斲丧,并进步系统的团体性能。
5.1 实现特点对比
实现TaskPoolWorker内存模型线程隔断离,内存不共享。线程隔断离,内存不共享。参数通报机制接纳尺度的结构化克隆算法(Structured Clone)举行序列化、反序列化,完成参数通报。支持ArrayBuffer转移和SharedArrayBuffer共享。接纳尺度的结构化克隆算法(Structured Clone)举行序列化、反序列化,完成参数通报。支持ArrayBuffer转移和SharedArrayBuffer共享。参数通报直接通报,无需封装,默认举行transfer。消息对象唯一参数,需要自己封装。方法调用直接将方法传入调用。在Worker线程中举行消息解析并调用对应方法。返回值异步调用后默认返回。主动发送消息,需在onmessage解析赋值。生命周期TaskPool自行管理生命周期,无需关心任务负载高低。开发者自行管理Worker的数量及生命周期。任务池个数上限自动管理,无需设置。同个进程下,最多支持同时开启64个Worker线程,实际数量由进程内存决定。任务实行时长上限3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),长时任务无实行时长上限。无穷制。设置任务的优先级支持设置任务优先级。不支持。实行任务的取消支持取消已经发起的任务。不支持。线程复用支持。不支持。任务延时实行支持。不支持。设置任务依靠关系支持。不支持。串行队列支持。不支持。任务组支持。不支持。 5.2 适用场景对比
由于TaskPool的工作线程会绑定系统的调度优先级,并且支持负载均衡(自动扩缩容),而Worker需要开发者自行创建,存在创建耗时以及不支持设置调度优先级,故在性能方面利用TaskPool会优于Worker,因此大多数场景保举利用TaskPool。
常见的一些开发场景及适用具体说明如下:
[*]运行时间超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时)的任务。例如后台举行1小时的预测算法训练等CPU密集型任务,需要利用Worker。
[*]有关联的一系列同步任务。例如在一些需要创建、利用句柄的场景中,句柄创建每次都是不同的,该句柄需永久生存,保证利用该句柄举行操作,需要利用Worker。
[*]需要设置优先级的任务。例如图库直方图绘制场景,后台盘算的直方图数据会用于前台界面的显示,影响用户体验,需要高优先级处置惩罚,需要利用TaskPool。
[*]需要频繁取消的任务。例如图库大图欣赏场景,为提升体验,会同时缓存当前图片左右侧各2张图片,往一侧滑动跳到下一张图片时,要取消另一侧的一个缓存任务,需要利用TaskPool。
[*]大量或者调度点较分散的任务。例如大型应用的多个模块包含多个耗时任务,不方便利用Worker去做负载管理,保举接纳TaskPool。
[*]对于需要频繁数据库操作的场景,由于读写数据库存在耗时,因此保举在子线程中操作,避免阻塞UI线程,保举利用TaskPool。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]