跟着鸿蒙生态的开展,鸿蒙开发已成为年代新风口,学习鸿蒙开发势在必行。鸿蒙开发可参阅学习文档:https://qr21.cn/FV7h05
简介
在OpenHarmony应用中,每个 进程 都会有一个主线程,主线程首要承当履行UI绘制操作、办理ArkTS引擎实例的创立和毁掉、分发和处理事件、办理Ability生命周期等责任,具体可拜见 线程模型概述 。因而,开发应用时应当尽量避免将耗时的操作放在主线程中履行。ArkTS供给了Worker和TaskPool两种多线程并发才能,多线程并发允许在同一时刻段内一起履行多段代码,这两个并发的根本才能可拜见 TaskPool和Worker的比照 。
在介绍Worker和TaskPool的具体运用办法前,咱们先简单介绍并发模型的相关概念,以便于咱们的理解。
并发模型概述
并发的意思是多个使命一起履行。并发模型分为两大类:依据内存同享的并发模型和依据音讯传递的并发模型。
在依据内存同享的并发模型中,并发线程经过读写内存中的同享目标来进行交互。依据同享内存的并发编程需求满足三条性质:
- 原子性:指一个操作是不可中止的,要么悉数履行成功要么悉数履行失利。
- 有序性:指程序履行的次序必须符合预期,不能呈现乱序的情况。
- 可见性:指当一个线程修改了同享变量后,其他线程能够立即得知这个修改。
现代程序语言一般经过锁、内存屏障、原子指令来满足这三条性质。依据内存同享的并发模型与底层硬件挨近,在能正确撰写并发代码的情况下,能够最大发挥底层硬件功用,完成功用优异的多线程程序。可是这种并发模型难以把握,即便资深的程序员也非常容易犯错。典型的依据内存同享并发模型的程序语言有C++ 、Swift和Java等。
在依据音讯传递的并发模型中,并发线程的内存相互隔离,需求经过通讯通道相互发送音讯来进行交互。典型的依据音讯传递的并发模型一般有两种:CSP和Actor。
CSP(Communicating Sequential Processes,通讯次序进程)中的计算单元并不能直接互相发送信息。需求经过通道(Channel)作为媒介进行音讯传递:发送方需求将音讯发送到Channel,而接纳方需求从Channel读取音讯。与CSP不同,在Actor模型中,每个Actor能够看做一个独立的计算单元,而且相互之间内存隔离,每个Actor中存在信箱(Mail Box),Actor之间能够直接进行音讯传递,如下图所示:
图1Actor音讯传递暗示图
CSP与Actor之间的首要差异:
- Actor需求明确指定音讯接纳方,而CSP中处理单元不用关心这些,只需求把音讯发送给Channel,而接纳方只需求从Channel读取音讯。
- 由于在默认情况下Channel是没有缓存的,因而对Channel的发送(Send)动作是同步堵塞的,直到另外一个持有该Channel引证的履行块取出音讯,而Actor模型中信箱本质是行列,因而音讯的发送和接纳能够是异步的。
典型的依据音讯传递的并发模型的程序语言有:Dart、JS和ArkTS。OpenHarmony中Worker和TaskPool都是依据Actor并发模型完成的并发才能。
Worker
根本概念和运作原理
OpenHarmony中的Worker是一个独立的线程,根本概念可拜见 TaskPool和Worker的比照 。Worker具有独立的运转环境,每个Worker线程和主线程相同具有自己的内存空间、音讯行列(MessageQueue)、事件轮询机制(EventLoop)、调用栈(CallStack)等。线程之间经过音讯(Massage)进行交互,如下图所示:
图2线程交互暗示图
在多核的情况下(下图中的CPU 1和CPU 2一起工作),多个Worker线程(下图中的worker thread1和worker thread2)能够一起履行,因而Worker线程做到了真实的并发,如下图所示:
图3Worker线程并发暗示图
运用场景和开发示例
关于Worker,有以下适用场景:
-
运转时刻超越3分钟的使命,需求运用Worker。
-
有相关的一系列同步使命,例如数据库增、删、改、查等,要确保同一个句柄,需求运用Worker。
以视频解压的场景为例,点击右上角下载按钮,该示例会履行网络下载并监听,下载完成后自动履行解压操作。当视频过大时,可能会呈现解压时长超越3分钟耗时的情况,因而咱们选用该场景来阐明怎样运用Worker。
场景预览图如下所示:
图4场景预览图
运用过程如下:
- 宿主线程创立一个Worker线程。经过
new worker.ThreadWorker()创立Worker实例,示例代码如下:
// 引进worker模块
import worker, { MessageEvents } from '@ohos.worker';
import type common from '@ohos.app.ability.common';
let workerInstance: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/pages/workers/worker.ts', {
name: 'FriendsMoments Worker'
});
2. 宿主线程给Worker线程发送使命音讯。宿主线程经过postMessage办法来发送音讯给Worker线程,发动下载解压使命,示例代码如下:
// 恳求网络数据
let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
// 参数中mediaData和isImageData是依据开发者自己的事务需求添加的,其中mediaData为数据路径、isImageData为判别图片或视频的标识
workerInstance.postMessage({ context, mediaData: this.mediaData, isImageData: this.isImageData });
3. Worker线程监听宿主线程发送的音讯。Worker线程在onmessage中接纳到宿主线程的postMessage恳求,履行下载解压使命,示例代码如下:
// 引进worker模块
import worker, { MessageEvents } from '@ohos.worker';
let workerPort = worker.workerPort;
// 接纳宿主线程的postMessage恳求
workerPort.onmessage = (e: MessageEvents): void => {
// 下载视频文件
let context: common.UIAbilityContext = e.data.context;
let filesDir: string = context.filesDir;
let time: number = new Date().getTime();
let inFilePath: string = `${filesDir}/${time.toString()}.zip`;
let mediaDataUrl: string = e.data.mediaData;
let urlPart: string = mediaDataUrl.split('.')[1];
let length: number = urlPart.split('/').length;
let fileName: string = urlPart.split('/')[length-1];
let options: zlib.Options = {
level: zlib.CompressLevel.COMPRESS_LEVEL_DEFAULT_COMPRESSION
};
request.downloadFile(context, {
url: mediaDataUrl,
filePath: inFilePath
}).then((downloadTask) => {
downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
Logger.info(`receivedSize:${receivedSize},totalSize:${totalSize}`);
});
downloadTask.on('complete', () => {
// 下载完成之后履行解压操作
zlib.decompressFile(inFilePath, filesDir, options, (errData: BusinessError) => {
if (errData !== null) {
...
// 反常处理
}
let videoPath: string = `${filesDir}/${fileName}/${fileName}.mp4`;
workerPort.postMessage({ 'isComplete': true, 'filePath': videoPath });
})
});
downloadTask.on('fail', () => {
...
// 反常处理
});
}).catch((err) => {
...
// 反常处理
});
};
4. 宿主线程监听Worker线程发送的信息。宿主线程经过onmessage接纳到Worker线程发送的音讯,并履行下载的成果通知。
- 开释Worker资源。在事务完成或许页面毁掉时,调用workerPort.close()接口主动开释Worker资源,示例代码如下所示:
workerInstance.onmessage = (e: MessageEvents): void => {
if (e.data) {
this.downComplete = e.data['isComplete'];
this.filePath = e.data['filePath'];
workerInstance.terminate();
setTimeout(() => {
this.downloadStatus = false;
}, LOADING_DURATION_OPEN);
}
};
TaskPool
根本概念和运作原理
相比运用Worker完成多线程并发,TaskPool愈加易于运用,创立开销也少于Worker,而且Worker线程有个数束缚,需求开发者自己把握,TaskPool的根本概念可拜见 TaskPool和Worker的比照 。TaskPool作用是为应用程序供给一个多线程的运转环境。TaskPool在Worker之上完成了调度器和Worker线程池,TaskPool依据使命的优先级,将其放入不同的优先级行列,调度器会依据自己完成的调度算法(优先级,防饥饿),从优先级行列中取出使命,放入TaskPool中的Worker线程池,履行相关使命,流程图如下所示:
图5TaskPool流程暗示图
TaskPool有如下的特点:
- 轻量化的并行机制。
- 降低全体资源的耗费。
- 进步体系的全体功用。
- 无需关心线程实例的生命周期。
- 能够运用TaskPool API创立后台使命(Task),并对所创立的使命进行如使命履行、使命取消的操作。
- 依据使命负载动态调节TaskPool工作线程的数量,以使使命依照预期时刻完成使命。
- 能够设置使命的优先级。
- 能够设置使命组(TaskGroup)将使命相关起来。
运用场景和开发示例
TaskPool的适用场景首要分为如下三类:
- 需求设置优先级的使命。
- 需求频频取消的使命。
- 很多或许调度点较涣散的使命。
由于朋友圈场景存在不同老友一起上传视频图片,在频频滑动时将屡次触发下载使命,所以下面将以运用朋友圈加载网络数据而且进行解析和数据处理的场景为例,来演示怎样运用TaskPool进行很多或调度点较涣散的使命开发和处理。场景的预览图如下所示:
图6朋友圈场景预览图
运用过程如下:
- 首要import引进TaskPool模块,TaskPool的API介绍可拜见 @ohos.taskpool(发动TaskPool) 。
import taskpool from '@ohos.taskpool';
2. new一个task目标,其中传入被调用的办法和参数。
...
// 创立task使命项,参数1.使命履行需求传入函数 参数2.使命履行传入函数的参数 (本示例中此参数为被调用的网络地址字符串)
let task: taskpool.Task = new taskpool.Task(getWebData, jsonUrl);
...
// 获取网络数据
@Concurrent
async function getWebData(url: string): Promise<Array<FriendMoment>> {
try {
let webData: http.HttpResponse = await http.createHttp().request(
url,
{ header: {
'Content-Type': 'application/json'
},
connectTimeout: 60000, readTimeout: 60000
})
if (typeof (webData.result) === 'string') {
// 解析json字符串
let jsonObj: Array<FriendMoment> = await JSON.parse(webData.result).FriendMoment;
let friendMomentBuckets: Array<FriendMoment> = new Array<FriendMoment>();
// 下方源码省略,首要为数据解析和耗时操作处理
...
return friendMomentBuckets;
} else {
// 反常处理
...
}
} catch (err) {
// 反常处理
...
}
}
3. 之后运用taskpool.execute履行TaskPool使命,将待履行的函数放入TaskPool内部使命行列等待履行。execute需求两个参数:创立的使命目标、等待履行的使命组的优先级,默认值是Priority.MEDIUM。在TaskPool中履行完数据下载、解析和处理后,再返回给主线程中。
let friendMomentArray: Array<FriendMoment> = await taskpool.execute(task, taskpool.Priority.MEDIUM) as Array<FriendMoment>;
4. 将新获取的momentData经过AppStorage.setOrCreate传入页面组件中。
// 获取页面组件中的momentData目标,其中是组件所需的username、image、video等数据
let momentData = AppStorage.get<FriendMomentsData>('momentData');
// 循环遍历目标并依次传入momentData
for (let i = 0; i < friendMomentArray.length; i++) {
momentData.pushData(friendMomentArray[i]);
}
// 将更新的momentData返回给页面组件
AppStorage.setOrCreate('momentData', momentData);
其他场景示例和计划思考
在日常开发过程中,咱们还会碰到一些其他并发场景问题,下面咱们介绍了常用并发场景的示例计划推荐。
Worker线程调用主线程类型的办法
咱们在主线程中创立了一个目标,假设类型为MyMath,咱们需求把这个目标传递到Worker线程中,然后在Worker线程中履行该类型中的一些耗时操作办法,比方Math中的compute办法,类结构示例代码如下:
class MyMath {
a: number = 0;
b: number = 1;
constructor(a: number, b: number) {
this.a = a;
this.b = b;
}
compute(): number {
return this.a + this.b;
}
}
主线程代码:
private math: MyMath = new MyMath(2, 3); // 初始化a和b的值为2和3
private workerInstance: worker.ThreadWorker;
this.workerInstance = new worker.ThreadWorker("entry/ets/worker/MyWorker.ts");
this.workerInstance.postMessage(this.math); // 发送到Worker线程中,希望履行MyMath中的compute办法,预期值是2+3=5
MyMath目标在进行线程传递后,会丢掉类中的办法特点,导致咱们只是在Worker线程中能够获取到MyMath的数据,可是无法在子体系中直接调用MyMath的compute办法,暗示代码如下:
const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {
let a = e.data.a;
let b = e.data.b;
}
这种情况下咱们能够怎样去完成在Worker线程中调用主线程中类的办法呢?
首要,咱们测验运用强制转化的办法把Worker线程接纳到数据强制转化成MyMath类型,示例代码如下:
const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {
let math = e.data as MyMath; // 办法一:强制转化
console.log('math compute:' + math.compute()); // 履行失利,不会打印此日志
}
强制转化后履行办法失利,不会打印此日志。由于序列化传输一般目标时,仅支持传递特点,不支持传递其原型及办法。接下来咱们测验第二种办法,依据数据从头初始化一个MyMath目标,然后履行compute办法,示例代码如下:
const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {
// 从头结构原类型的目标
let math = new MyMath(0, 0);
math.a = e.data.a;
math.b = e.data.b;
console.log('math compute:' + math.compute()); // 成功打印出成果:5
}
第二种办法成功在Worker线程中调用了MyMath的compute办法。可是这种办法还有坏处,比方每次运用到这个类进行传递,咱们就得从头进行结构初始化,而且结构的代码会涣散到工程的遍地,很难进行保护,所以咱们有了第三种改进计划。
第三种办法,咱们需求结构一个接口类,包含了咱们需求线程间调用的基础办法,这个接口类首要是办理和束缚MyMath类的功用标准,确保MyMath类和它的代理类MyMathProxy类在主线程和子线程的功用一致性,示例代码如下:
interface MyMathInterface {
compute():number;
}
然后,咱们把MyMath类承继这个办法,而且额定结构一个代理类,承继MyMath类,示例代码如下:
class MyMath implements MyMathInterface {
a: number = 0;
b: number = 1;
constructor(a: number, b: number) {
console.log('MyMath constructor a:' + a + ' b:' + b)
this.a = a;
this.b = b;
}
compute(): number {
return this.a + this.b;
}
}
class MyMathProxy implements MyMathInterface {
private myMath: MyMath;
constructor(math: MyMath) {
this.myMath = new MyMath(math.a, math.b);
}
// 代理MyMath类的compute办法
compute(): number {
return this.myMath.compute();
}
}
咱们在主线程结构而且传递MyMath目标后,在Worker线程中转化成MyMathProxy,即可调用到MyMath的compute办法了,而且无需在多处进行初始化结构,只要把结构逻辑放到MyMathProxy或许MyMath的结构函数中,Worker线程中的示例代码如下:
const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {
// 办法三:运用代理类结构目标
let proxy = new MyMathProxy(e.data)
console.log('math compute:' + proxy.compute()); // 成功打印出成果:5
}
咱们能够依据实际场景选择第二种或许第三种计划。
跟着鸿蒙生态的开展,鸿蒙开发已成为年代新风口,学习鸿蒙开发势在必行。鸿蒙开发可参阅学习文档:https://qr21.cn/FV7h05






