【鸿蒙实战开发】ArkTS多线程的多线程系列(四):基于生产者-消费者实现多线程协同

日志记录、埋点是较为常见的生产者-消费者模式使用场景,应用的主线程以及其他业务子线程作为生产者,在需要的时候向共享队列中插入日志或者埋点数据,消费者子线程则会从队列中将日志或者埋点信息取出,并进行相关业务处理,比如落盘、上云等。这样所有的日志、埋点处理均在子线程中完成,不占用主线程执行时间。并且业务独立,可以通过独立的团队进行开发,其他业务团队只需要向队列中插入数据即可。

生产者-消费者模式简介

生产者-消费者模式是一种经典的多线程设计模式,它为多线程间的协同提供了良好的解决方案。

在生产者-消费者模式中,通常有两类线程,即若干个生产者线程和若干个消费者线程。生产者线程负责提交用户请求,消费线程负责具体处理生产者提交的任务。生产者和消费者直接则通过共享内存缓冲区进行通信。

从图中可以得看出生产者-消费者模式的如下优点

  • 性能提升:将一个耗时的流程拆成生产和消费两个阶段。

  • 业务解耦:共享内存缓冲区作为生产者线程和消费者线程间的通信桥梁,避免了生产者线程与消费者线程直接通信,从而将两类线程进行解耦。也就是说生产者不需要知道消费者的存在,反之消费者也不需要知道生产者的存在。

  • 性能平衡:共享内存缓冲区的设计也可以很好的解决生产者与消费者线程间的性能上的差异,可以通过调整慢的一方的并发数,从而提高任务的整体处理速度。

生产者-消费者模式的实现

在ArkTS上由于没有java的wait/notify机制(线程挂起并释放锁,线程唤醒),因此在ArkTS上只能通过同步队列的方式实现生产者-消费者模式。

同步队列方案中,我们需要构建一个队列来作为共享缓冲区,生产者线程通过执行offer操作,向缓冲区中添加数据;消费者线程从通过poll操作从缓冲区取出数据。

在实现生产者-消费者模式之前,我们需要先完成内存缓存区的开发。

共享内存缓冲区的实现

共享内存缓冲需要被多个线程使用(offer、poll),因此需要使用Sendable实现、。在生产者-消费者模式中,缓冲区采用FIFO的方式进行队列管理,又由于多个线程都需要对队列进行操作,因此对于队列的操作必须是线程安全的。

基于以上思想,我们需要先实现节点对象ShareNode,由于需要在多个线程间共享,所以他必须是Sendble类型变量。其中item表示节点的指,next为指向下一个节点的指针。

@Sendable

export class ShareNode<E> {

item : E | null = null;

next : ShareNode<E> | null = null

}

接下来就可以实现队列类ShareQueue了,由于需要在多个线程间共享,所以他必须是Sendble类型变量。head为队首元素,初始值为null,当插入第一个元素时为其赋值,随着队首元素的移出修改指向的对象,具体参考poll方法的实现tail为对位元素,初始值为null,当插入第一个元素时为其赋值,并随着新元素的插入修改指向的对象,具体参考offer方法的实现。

import { ShareNode } from './ShareNode';

import utils from '@arkts.utils';

import { ArkTSUtils} from '@kit.ArkTS';

@Sendable

export class ShareQueue<E> {

private head : ShareNode<E> | null = null

private tail : ShareNode<E> | null = null

constructor() {

}

public async offer(e : E) : Promise<boolean> {

let lock: utils.locks.AsyncLock = utils.locks.AsyncLock.request('ShareQueue');

return lock.lockAsync(() => {

let node = new ShareNode<E>();

node.item = e;

if (this.tail != null) {

this.tail.next = node;

this.tail = node

return true;

} else {

// 首元素加入队列时,队首和队尾元素都指向首元素

this.head = node;

this.tail = node;

return true;

}

}, ArkTSUtils.locks.AsyncLockMode.EXCLUSIVE)

}

public async poll() : Promise<E|null> {

let lock: utils.locks.AsyncLock = utils.locks.AsyncLock.request('ShareQueue');

return lock.lockAsync(() => {

let p : ShareNode<E> | null = this.head;

if (p != null) {

let item : E | null = p.item;

this.head = p.next;

// 当队列全部取出时,将队尾元素一同置空。

if (this.head == null) {

this.tail = null

}

return item;

} else {

return null;

}

}, ArkTSUtils.locks.AsyncLockMode.EXCLUSIVE)

}

代码中使用了AsyncLock对临界区进行加锁,offer和poll方法的实现也不复杂。

  • 当调用offer方法新增元素节点时,需要新建一个ShareNode对象,并将当前队尾元素tail的next指针指向新元素,之后再将tail = node指向新元素,实现新元素的入队。

  • 当调用poll方法获取队首元素节点时,需要先将队首元素head赋值给中间变量p,并将p的next属性指向的元素(即第二个元素)的指针赋值给head = p.next,并返回原先的队首元素,从而实现了队首元素的向后移动。

从代码可以看出,此处构建的ShareQueue是一个无限长的队列。

消费者线程实现

结合业务场景,消费者需要不断的从队列中获取业务数据(日志信息、埋点信息),因此消费者线程应该是一个长期存在的线程,它贯穿于整个应用的生命周期,因此消费者线程可以选择Worker或者LongTask来实现(案例采用LongTask实现)。以下为子线程执行业务逻辑代码:

@Concurrent

export async function consumerTask(taskName : string, sq : ShareQueue<LogInfo>, cc : LongTaskController) {

let unused = ArkTSUtils.locks.AsyncLock;

while (true) {

if (cc.command == LongTaskCommandEnum.STOP) {

break;

}

let start : number;

start = Date.now();

while (Date.now() - start < 100) {

// 模拟等待0.1秒

}

let logInfo = await sq.poll()

if (logInfo != null) {

taskpool.Task.sendData(logInfo);

}

}

}

其中LogInfo是从队列里面拿到的日志信息,并在任务中做了0.1秒的模拟等待,当从ShareQueue中获取到日志信息后,将日志发送给主线程刷新UI。由于消费者线程是一个通过new taskpool.LongTask()构建的长时任务,且子线程一直处于while(true)的循环中,因此如果需要停止线程,需要将控制命令LongTaskController状态置为LongTaskCommandEnum.STOP,并在线程结束的promise回调中调用taskpool.terminateTask()将长时任务停掉。以下为消费者长时线程任务常见管理实现代码片段:

@Component

export struct Producer_ConsumerPage {

private consumer_one !: taskpool.LongTask ;

private sq : ShareQueue<LogInfo> = new ShareQueue<LogInfo>()

@State logInfo : string = "";

...

build() {

NavDestination() {

Column() {

...

Button('一个消费者线程消费').onClick(event => {

this.consumerController_1.command = LongTaskCommandEnum.STOP

this.consumerController_1 = new LongTaskController();

this.startComsumer("consumer_one", this.consumer_one, this.consumerController_1);

})

TextArea({text: this.logInfo}).focusable(false).height('50%').width('80%').alignSelf(ItemAlign.Center).fontSize(10)

}

}

}

private startComsumer(taskName : string, consumer : taskpool.LongTask, lct : LongTaskController) {

lct.command = LongTaskCommandEnum.START;

consumer = new taskpool.LongTask(taskName, consumerTask, taskName, this.sq, lct);

consumer.onReceiveData(async (logInfo: LogInfo) => {

let log = taskName + " : " + logInfo.logMsg + "\n";

this.logInfo = log + this.logInfo;

});

taskpool.execute(consumer).then(() => {

taskpool.terminateTask(consumer);

});

}

}

案例中还模拟实现了多个消费者线程的场景,实际业务中,可以结合共享缓冲区内的信息数量,需要的动态新增消费者线程数,以确保共享缓冲区内积压的消息数量不要过多;当消息较少时,则可以减少消费者线程,释放线程资源。

生产者线程实现

结合业务场景,生产者可能是任何线程,比如UI主线程、或者任意子线程。因此案例中提供了主线程作为生产者、普通Task作为生产者、LongTask作为生产者。主线程(UI线程)生产日志:

@Component

export struct Producer_ConsumerPage {

private sq : ShareQueue<LogInfo> = new ShareQueue<LogInfo>()

build() {

NavDestination() {

Column() {

Button('主线程生产一条日志').onClick(event => {

let logInfo : LogInfo = new LogInfo("mainThrea", LogLevelEnum.INFO, "this is a log from Producer_ConsumerPage")

this.sq.offer(logInfo);

})

...

}

.justifyContent(FlexAlign.SpaceEvenly)

.height('100%')

.width('100%')

}

}

}

子线程生产日志

@Component

export struct Producer_ConsumerPage {

private producer !: taskpool.Task;

private sq : ShareQueue<LogInfo> = new ShareQueue<LogInfo>(10)

build() {

NavDestination() {

Column() {

Button('十个子线程生产十条日志').onClick(event => {

let i = 0;

for (i = 0; i < 10; i++) {

taskpool.execute(this.producer);

}

})

...

}

}

.onAppear( () => {

this.producer = new taskpool.Task("producer", producerTask, "producer", this.sq);

})

}

@Concurrent

export async function producerTask(taskName : string, sq : ShareQueue<LogInfo>) {

let logInfo : LogInfo;

logInfo= new LogInfo(taskName, LogLevelEnum.DEBUGGER, "log from " + taskName)

await sq.offer(logInfo)

}

除此之外也可以用LongTask进行生产,此处不再赘述,可以参考消费者线程的长时任务构建和停止逻辑。

鸿蒙全栈开发全新学习指南

为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线【包含了大厂APP实战项目开发】

本路线共分为四个阶段

第一阶段:鸿蒙初中级开发必备技能

在这里插入图片描述

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

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

开发基础知识:gitee.com/MNxiaona/733GH

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

基于ArkTS 开发

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

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:gitee.com/MNxiaona/733GH

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值