鸿蒙(HarmonyOS)性能优化实战-多线程共享内存

概述

在应用开发中,为了避免主线程阻塞,提高应用性能,需要将一些耗时操作放在子线程中执行。此时,子线程就需要访问主线程中的数据。ArkTS采用了基于消息通信的Actor并发模型,具有内存隔离的特性,所以跨线程传输数据时需要将数据序列化,但是AkrTS支持通过可共享对象SharedArrayBuffer实现直接的共享内存。

在开发应用时,如果遇到数据量较大,并且需要多个线程同时操作的情况,推荐使用SharedArrayBuffer共享内存,可以减少数据在线程间传递时需要复制和序列化的额外开销。比如,音视频解码播放、多个线程同时读取写入文件等场景。由于内存是共享的,所以在多个线程同时操作同一块内存时,可能会引起数据的紊乱,这时就需要使用锁来确保数据操作的有序性。本文将基于此具体展开说明。关于多线程的使用和原理,可参考OpenHarmony多线程能力场景化示例实践,本文将不再详细讲述。

工作原理

可共享对象SharedArrayBuffer,是拥有固定长度的原始二进制数据缓冲区,可以存储任何类型的数据,包括数字、字符串等。它支持在多线程之间传递,传递之后的SharedArrayBuffer对象和原始的SharedArrayBuffer对象可以指向同一块内存,进而达到共享内存的目的。SharedArrayBuffer对象存储的数据在子线程中被修改时,需要通过原子操作保证其同步性,即下个操作开始之前务必需要保证上个操作已经结束。下面将通过示例说明原子操作保证同步性的必要性。

非原子操作

......
// 非原子操作,进行10000次++
@Concurrent
function normalProcess(int32Array: Int32Array) {for (let i = 0; i < 10000; i++) {int32Array[0]++;}
}
// 原子操作,进行10000次++
@Concurrent
function atomicsProcess(int32Array: Int32Array) {for (let i = 0; i < 10000; i++) {Atomics.add(int32Array, 0, 1);}
}
......
@State result: string = "计算结果:";
private taskNum: number = 2;
private scroller: Scroller = new Scroller();
......
Button("非原子操作").width("80%").fontSize(30).fontWeight(FontWeight.Bold).margin({ top: 30 }).onClick(async () => {this.sharedArrayBufferUsage(false);})
Scroll(this.scroller) {Column() {Text(this.result).width("80%").fontSize(30).fontWeight(FontWeight.Bold).fontColor(Color.Blue)}
}.height("60%")
.margin({ top: 30 })
......
// 根据传入的值isAtomics判断是否使用原子操作
sharedArrayBufferUsage(isAtomics: boolean) {// 创建长度为4的SharedArrayBuffer对象let sab: SharedArrayBuffer = new SharedArrayBuffer(4);// 由于SharedArrayBuffer是原始二进制数据缓冲区,无法直接使用,所以这里转换为Int32Array类型进行后续操作let int32Array: Int32Array = new Int32Array(sab);int32Array[0] = 0;let taskGroup: taskpool.TaskGroup = new taskpool.TaskGroup();// 创建Task对象,并放入TaskGroup中执行for (let i = 0; i < this.taskNum; i++) {if (isAtomics) {taskGroup.addTask(new taskpool.Task(atomicsProcess, int32Array));} else {taskGroup.addTask(new taskpool.Task(normalProcess, int32Array));}}taskpool.execute(taskGroup).then(() => {// 将结果打印在Text上this.result = this.result + "\n" + int32Array;// 如果Scroll不在最低端,则滑动到最低端if (!this.scroller.isAtEnd()) {this.scroller.scrollEdge(Edge.Bottom);}}).catch((e: BusinessError) => {logger.error(e.message);})
}

在这段代码中,创建了2个task,对SharedArrayBuffer分别进行了10000次自增操作,预期的结果应该是20000。点击按钮查看计算结果,就会发现最后的结果并不一定是20000,并且每次点击后,计算的结果都可能是不同的。
这是因为SharedArrayBuffer是共享内存的,多个线程同时进行自增时,是操作的同一块内存,而自增操作并不是原子操作,需要经过以下三个步骤:

  • 第一步,从内存中取值
  • 第二步,对取出的值+1
  • 第三步,将结果写入内存

当多个线程同时操作时,就会发生这样一种情况:A线程在第一步取值1000,第二步+1操作后是1001,在执行第三步之前,B线程也去取值了,这时由于A线程还没有将结果写入内存,所以B线程取到的值依然是1000,然后A执行第三步将1001写入了内存,而B会对1000进行+1操作并将结果1001写入同一块内存。这样就会导致明明进行了两次+1的操作,但是结果并没有变成预期的1002,而是1001。所以在这个示例中会出现结果不符合预期的情况。

原子操作

下面修改一下代码,将自增操作改为使用Atomics.add()方法的原子操作。

......
Button("原子操作").width("80%").fontSize(30).fontWeight(FontWeight.Bold).margin({ top: 30 }).onClick(async () => {this.sharedArrayBufferUsage(true);})
......

点击按钮查看计算结果,就会发现不论计算多少次,结果一直都是20000。这是因为,原子操作是不可中断的一个或者一系列操作,可以保证在A线程执行完取值、计算、写入内存这三个步骤之前,不会被B线程中断,也就不会发生非原子操作示例中B线程取到旧值的情况,而是每次都能拿到A线程写入内存的新值。所以,在使用SharedArrayBuffer共享内存时,一定要注意使用原子操作保证同步性,否则就可能会造成数据的紊乱。

场景示例

在应用开发中使用多线程时,会遇到处理复杂逻辑的情况,是无法保证整个线程都是一个原子操作的,此时就可以使用锁来解决一段代码的原子性问题。

锁的实现

并发编程重在解决线程间分工、同步与互斥的问题,而实现互斥的重要方式是通过锁。示例通过Atomics和SharedArrayBuffer简单实现不可重入锁类NonReentrantLock。
constructor()通过传入可共享对象SharedArrayBuffer初始化锁,实现多线程共享同一块内存,以作为共同操作的标志位,从而控制锁的状态。

const UNLOCKED = 0;
const LOCKED_SINGLE = 1;
const LOCKED_MULTI = 2;
export class NonReentrantLock {flag: Int32Array;constructor(sab: SharedArrayBuffer) { // 传入一个4bytes的SharedArrayBufferthis.flag= new Int32Array(sab); // 其视图为只有一位的int数组(1 = 4bytes * 8 / 32bit)}lock(): void {...}tryLock(): boolean {...}unlock(): void {...}
}

lock()方法用于获取锁,如果获取锁失败,则线程进入阻塞状态。

lock(): void {const flag= this.flag;let c = UNLOCKED;// 如果flag数组的0位置,当前值为UNLOCKED,则改为LOCKED_SINGLE;否则,进入do-while循环,阻塞线程if ((c = Atomics.compareExchange(flag, 0, UNLOCKED, LOCKED_SINGLE)) !== UNLOCKED) {do {// 有线程拿不到锁时,修改标志位为LOCKED_MULTI,并使之进入睡眠阻塞状态if (c === LOCKED_MULTI || Atomics.compareExchange(flag, 0, LOCKED_SINGLE, LOCKED_MULTI) !== UNLOCKED) {Atomics.wait(flag, 0, LOCKED_MULTI);}// 被唤醒的线程,如果还是没有拿到锁,就回到循环中,重新进入阻塞状态} while ((c = Atomics.compareExchange(flag, 0, UNLOCKED, LOCKED_MULTI)) !== UNLOCKED);}
}

tryLock()方法用于尝试获取锁,如果获取锁成功则返回true,失败返回false,但不会阻塞线程。

tryLock(): boolean {const flag= this.flag;return Atomics.compareExchange(flag, 0, UNLOCKED, LOCKED_SINGLE) === UNLOCKED;
}

unlock()方法用于释放锁。

unlock(): void {// 局部化flag,保证只有获取锁的线程可以释放锁const flag= this.flag;let v0 = Atomics.sub(flag, 0, 1);if (v0 !== LOCKED_SINGLE) {Atomics.store(flag, 0, UNLOCKED);// 只唤醒在数组0索引位置等待的其中一个线程,去上方lock()方法while条件中检测,尝试获取锁Atomics.notify(flag, 0, 1);}
}

锁的应用

示例通过多线程写入文件的场景,展示多线程不合理操作共享内存时,出现的线程不安全问题,进而导致输出文件乱码的情况。并通过使用上文实现的NonReentrantLock,解决该问题。
主线程通过startWrite(useLock: boolean)方法,开启多线程写入文件,并通过useLock参数控制是否使用锁。

@Component
export struct LockUsage {taskNum: number = 10; // 任务数,实际并行线程数依设备而定baseDir: string = getContext().filesDir + '/TextDir'; // 文件写入的应用沙箱路径sabInLock: SharedArrayBuffer = new SharedArrayBuffer(4); // 在主线程,初始化子线程锁标志位,所使用的共享内存sabForLine: SharedArrayBuffer = new SharedArrayBuffer(4); // 在主线程,初始化子线程偏移位,所使用的共享内存@State result: string = "";build() {Row() {Column() {// 不使用锁写入的按钮Button($r('app.string.not_use_lock')).width("80%").fontSize(30).fontWeight(FontWeight.Bold).margin({ top: 30 }).onClick(async () => {this.startWrite(false);})// 使用锁写入的按钮Button($r('app.string.use_lock')).width("80%").fontSize(30).fontWeight(FontWeight.Bold).margin({ top: 30 }) .onClick(async () => {this.startWrite(true);})// 运行状态说明Text(this.result).width("80%").fontSize(30).fontWeight(FontWeight.Bold).fontColor(Color.Blue).margin({ top: 30 }) }.width('100%')}.height('100%')}startWrite(useLock: boolean): void {// 指明运行状态为“写入文件开始”this.result = getContext().resourceManager.getStringSync($r('app.string.write_file_start'));  // 初始化写入时的偏移量let whichLineToWrite: Int32Array = new Int32Array(this.sabForLine);Atomics.store(whichLineToWrite, 0, 0);// 开启多线程依据偏移量指定位置写入文件// 通过主线程的sabInLock:SharedArrayBuffer初始化锁,保证多线程操作同一处锁标志位// 通过主线程的sabForLine:SharedArrayBuffer初始化偏移位,保证多线程操作同一处偏移位置let taskPoolGroup: taskpool.TaskGroup = new taskpool.TaskGroup();for (let i: number = 0; i < this.taskNum; i++) {taskPoolGroup.addTask(new taskpool.Task(createWriteTask, this.baseDir, i, this.sabInLock, this.sabForLine, useLock));}taskpool.execute(taskPoolGroup).then(() => {// 指明运行状态为“写入文件成功”this.result = this.result = getContext().resourceManager.getStringSync($r('app.string.write_file_success'));  }).catch(() => {// 指明运行状态为“写入文件失败”this.result = getContext().resourceManager.getStringSync($r('app.string.write_file_failed'));  })}
}

子线程根据偏移量在指定位置写入文件,并通过偏移量自增,指定下次的写入位置。

@Concurrent
async function createWriteTask(baseDir: string, writeText: number, sabInLock: SharedArrayBuffer, sabForLine: SharedArrayBuffer, useLock: boolean): Promise<void> {class Option { // 写入文件时的接口方法参数类offset: number = 0;length: number = 0;encoding: string = 'utf-8';constructor(offset: number, length: number) {this.offset = offset;this.length = length;}}// 初始化输出文件目录let filePath: string | undefined = undefined;filePath = baseDir + useLock ? "/useLock.txt" : "/unusedLock.txt";if (!fs.accessSync(baseDir)) {fs.mkdirSync(baseDir);}// 利用主线程传入的SharedArrayBuffer初始化锁let nrl: NonReentrantLock | undefined = undefined;if (useLock) {nrl = new NonReentrantLock(sabInLock);}// 利用主线程传入的SharedArrayBuffer初始化写入文件时的偏移量let whichLineToWrite: Int32Array = new Int32Array(sabForLine);let str: string = writeText + '\n';for (let i: number = 0; i < 100; i++) {// 获取锁if (useLock && nrl !== undefined) {nrl.lock();}// 写入文件let file: fs.File = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);try {fs.writeSync(file.fd, str, new Option(whichLineToWrite[0], str.length));} catch (err) {logger.error(`errorCode : ${err.code},errMessage : ${err.message}`);}fs.closeSync(file);// 修改偏移量,指定下次写入时的位置whichLineToWrite[0] += str.length;// 释放锁if (useLock && nrl !== undefined) {nrl.unlock();}}
}

从应用沙箱地址查看写入的文件,可以看到unusedLock.txt文件,所写行数不足1000行,且存在乱码,如图1所示。

图1 不使用锁写入的文件

而usedLock.txt文件,所写行数刚好1000行,且不存在乱码,如图2所示。

图2 使用锁写入的文件

总结

综上所述,虽然使用了基于消息通信的Actor并发模型,但是ArkTS依旧支持通过共享内存的方式进行线程间通信。同时,在使用SharedArrayBuffer进行共享内存时,也需要通过原子操作或者锁来解决线程间同步与互斥的问题。合理使用多线程共享内存,才能在保证线程安全的前提下,提升应用的性能。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2980126.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

4.23日总结(项目总结)

1.项目&#xff1a; 今日项目通过一个在登录界面的一个静态变量&#xff0c;完成了区分老师和学生&#xff0c;能够分开老师和学生&#xff0c;并且不同身份的人进去会有不同的显示&#xff0c;以及登录链接主界面&#xff0c;还有学生和老师的不同的表&#xff0c;其次就是创…

葡萄书--关系图卷积神经网络

异质图和知识图谱 同质图与异质图 同质图指的是图中的节点类型和关系类型都仅有一种 异质图是指图中的节点类型或关系类型多于一种 知识图谱 知识图谱包含实体和实体之间的关系&#xff0c;并以三元组的形式存储&#xff08;<头实体, 关系, 尾实体>&#xff0c;即异…

用Excel做一个功能完备的仓库管理系统

1 基本设计思路 用到的Excel技术&#xff1a;sumif, vlookup, 表格(table)。基本思路&#xff1a;在有基础的商品、仓库等信息的情况下&#xff0c;对商品的每一个操作都有对应的单据&#xff0c;然后再汇总统计。标识&#xff1a;为了在不同的维度统计数量&#xff0c;各单据…

Abaqus2024 安装教程(附免费安装包资源)

鼠标右击软件压缩包&#xff0c;选择“解压到Abaqus2024”。 鼠标右击“此电脑”&#xff0c;选择“属性”。 点击“高级系统设置”。 点击“环境变量”。 点击“新建”。 变量名输入&#xff1a;NOLICENSECHECK 变量值输入&#xff1a;true 然后点击“确定”。 点击“确定”。…

羊大师解析,夏日消暑羊奶来帮忙

羊大师解析&#xff0c;夏日消暑羊奶来帮忙 炎炎夏日&#xff0c;烈日当空&#xff0c;人们总是寻找各种方式来消暑降温。除了常见的冷饮、空调等&#xff0c;其实还有一种天然、健康的饮品可以帮助我们度过酷暑——那就是羊奶。 羊奶作为一种营养丰富的天然饮品&#xff0c;不…

【git】多仓库开发

通常我们习惯了在单个仓库下多分支开发&#xff0c;最近用了多仓库开发&#xff0c;我拿一个开源项目举例&#xff0c;总结一下基本流程。 1.fork项目到自己仓库 2.把自己仓库项目pull到本地 3.在本地新建开发分支 git checkout -b dev 4.修改/添加分支对应的远程仓库 修…

小案例:ToolBar+选项菜单

使用选项菜单&#xff0c;一般是用于做单选&#xff0c;需要重写方法&#xff1a; public boolean onCreateOptionsMenu(Menu menu) 如果想要实现事件监听&#xff0c;则采用基于回调的事件监听机制&#xff0c;可以监听到具体是哪一项被选中。即重写方法&#xff1a; publi…

Kafka学习笔记(二、linux和docker安装及使用demo)

1.安装启动 1.1.下载解压 官网下载地址将下载的tar包上传到服务器一个目录&#xff0c;然后解压$ tar -xzf kafka_2.13-3.7.0.tgz $ cd kafka_2.13-3.7.01.2. 启动环境 需安装Jdk8&#xff0c;Kafka可以使用ZooKeeper或KRaft启动。 ZooKeeper启动 运行如下命令&#xff0c;…

windows系统下python开发工具安装

一. 简介 前一篇文章学习了安装 python解释器&#xff0c;文章如下&#xff1a; windows系统下python解释器安装-CSDN博客 本文来学习如何下载安装 python开发工具 PyCharm。 二. python开发工具 PyCharm下载安装 1. PyCharm官网 PyCharm开发工具 PyCharm为 python代码…

【学习笔记二十四】EWM补货策略和自动补货配置

一、EWM补货策略概述 1.计划补货 ①以联机或批处理模式启动 ②根据最大和最小数量计算补货 ③仅当库存量低于最低数量时才开始 ④四舍五入至最小补货数量的倍数 2.自动补货 ①在WT确认期间启动 ②根据最大和最小数量计算补货 ③只有当库存量低于最低数量时才开始 ④四舍…

OpenWRT磁盘扩容(PVE虚拟机方案)

官方扩容指导文档 PVE给虚拟机磁盘扩容 给虚拟机磁盘扩容&#xff0c;选中OpenWRT的硬盘&#xff0c;随后选择调整大小 输入增量大小&#xff0c;即增加多少磁盘空间给硬盘。这里我选择增加4G 进入OpenWRT控制台界面安装一些linux常用查看磁盘的工具&#xff08;也可以通过网…

linux的一些实用操作

快捷键 强制停止 ctrlc强制停止或退出命令的输入 退出登出 ctrld强制退出用户登录或退出某些程序的专属页面&#xff08;如py&#xff09; ps&#xff1a;不能退出vi/vim 历史命令搜索 history可以查看历史命令&#xff0c;用来复制粘贴 在使用history之后&#xff0c;…

深度学习-数据预处理

目录 创建一个人工数据集处理缺失的数据插入对inputs中的类别值或离散值&#xff0c;将NaN视为一个类别对inputs和outputs中的数值类型转换为张量格式 创建一个人工数据集 import os import pandas as pd os.makedirs(os.path.join(.., data), exist_okTrue) data_file os.p…

【后端】PyCharm的安装指引与基础配置

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、PyCharm是什么二、PyCharm安装指引安装PyCharm社区版安装PyCharm专业版 三、配置PyCharm&#xff1a;四、总结 前言 随着开发语言及人工智能工具的普及&am…

sqlilabs靶场1—20题学习笔记(思路+解析+方法)

前几个题目较为简单&#xff0c;均尝试使用各种方法进行SQL注入 第一题 联合查询 1&#xff09;思路&#xff1a; 有回显值 1.判断有无注入点 2.猜解列名数量 3.判断回显点 4.利用注入点进行信息收集 爆用户权限&#xff0c;爆库&#xff0c;爆版本号 爆表&#xff0c;爆列&…

ABeam×StartUp丨蓝因机器人访问ABeam旗下德硕管理咨询(深圳)新创部门,展开合作交流

近日&#xff0c;深圳蓝因机器人科技有限公司&#xff08;以下简称“蓝因机器人”&#xff09;创始人陈卜铭先生来访ABeam旗下德硕管理咨询&#xff08;深圳&#xff09;有限公司&#xff08;以下简称“ABeam-SZ”&#xff09;&#xff0c;与新创部门展开合作交流。 交流中&am…

【无线通信】OQPSK

调制 sps 8; RolloffFactor 0.2; FilterSpanInSymbols 10;bits randi([0, 1], 224*8, 1); % 1792symbols bits*2 - 1; % 1792 re -symbols(2:2:end); % 896 im -symbols(1:2:end); % 896pFilterTx comm.RaisedCosineTransmitFilter(...Shape, Square root, ...Rollo…

Linux进程详解二:创建、状态、进程排队

文章目录 进程创建进程状态进程排队 进程创建 pid_t fork(void) 创建一个子进程成功将子进程的pid返回给父进程&#xff0c;0返回给新创建的子进程 fork之后有两个执行分支&#xff08;父和子&#xff09;&#xff0c;fork之后代码共享 bash -> 父 -> 子 创建一个进…

这届年轻人,对AI对象上头

在AI技术飞速发展的今天&#xff0c;虚拟对象这一概念也流行了起来。从AI女友到AI男友&#xff0c;这些基于人工智能的AI社交应用正在改变我们对情感陪伴的认知。本文深入探讨了AI虚拟对象的兴起、用户需求、商业模式以及技术局限&#xff0c;不妨来看一下。 继2023年文生文大语…

dremio支持设置

Dremio 支持提供可用于诊断目的的设置。这些设置通过 Dremio UI&#xff1a;设置>支持启用&#xff08;或禁用&#xff09; 使用 Client Tools 可以配置当用户查看数据集中的数据时&#xff0c;Dremio 项目的工具栏上显示哪些客户端应用程序按钮。用户可以通过单击相应的工具…