日志记录、埋点是较为常见的生产者-消费者模式使用场景,应用的主线程以及其他业务子线程作为生产者,在需要的时候向共享队列中插入日志或者埋点数据,消费者子线程则会从队列中将日志或者埋点信息取出,并进行相关业务处理,比如落盘、上云等。这样所有的日志、埋点处理均在子线程中完成,不占用主线程执行时间。并且业务独立,可以通过独立的团队进行开发,其他业务团队只需要向队列中插入数据即可。
生产者-消费者模式简介
生产者-消费者模式是一种经典的多线程设计模式,它为多线程间的协同提供了良好的解决方案。
在生产者-消费者模式中,通常有两类线程,即若干个生产者线程和若干个消费者线程。生产者线程负责提交用户请求,消费线程负责具体处理生产者提交的任务。生产者和消费者直接则通过共享内存缓冲区进行通信。
从图中可以得看出生产者-消费者模式的如下优点
-
性能提升:将一个耗时的流程拆成生产和消费两个阶段。
-
业务解耦:共享内存缓冲区作为生产者线程和消费者线程间的通信桥梁,避免了生产者线程与消费者线程直接通信,从而将两类线程进行解耦。也就是说生产者不需要知道消费者的存在,反之消费者也不需要知道生产者的存在。
-
性能平衡:共享内存缓冲区的设计也可以很好的解决生产者与消费者线程间的性能上的差异,可以通过调整慢的一方的并发数,从而提高任务的整体处理速度。
生产者-消费者模式的实现
在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进行生产,此处不再赘述,可以参考消费者线程的长时任务构建和停止逻辑。
鸿蒙全栈开发全新学习指南
有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以要有一份实用的鸿蒙(HarmonyOS NEXT)学习路线与学习文档用来跟着学习是非常有必要的。
针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。
本路线共分为四个阶段:
第一阶段:鸿蒙初中级开发必备技能
第二阶段:鸿蒙南北双向高工技能基础: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