HarmonyOS Next IM 实战: Worker 线程中模块未初始化异常处理

背景介绍
开发即时通讯 IM SDK 过程中遇到了用户反馈消息接收延迟,打开页面卡顿等问题,由于 IM 涉及到较多的网络请求和数据库操作等,而之前这些全部都放在了主线程,当涉及聊天内容与会话较多时,IO 操作会导致应用卡顿丢帧。最开始设计时考虑到使用 HarmonyOS 提供的 Worker 也 TaskPool 等多线程,但是由于时间关系和逻辑复杂,非共享内存方式的多线程有较大改造成本。现在生产环境遇到瓶颈,必须通过多线程机制解决了。
实现思路
消息接收方式采用推拉结合方式,当有新消息时通过长连接下发到客户端,客户端在通过 http 方式请求消息具体内容。在 Android 和 iOS 端采用了守护线程,一个专门拉取新消息的线程,当有事件时触发拉取消息接口调用,无事件时线程阻塞等待。
Worker 与 TaskPool 选择
内存共享并发模型指多线程同时执行任务,这些线程依赖同一内存并且都有权限访问,线程访问内存前需要抢占并锁定内存的使用权,没有抢占到内存的线程需要等待其他线程释放使用权再执行。常见的并发模型有基于内存共享的模型和基于消息通信的模型。Actor 并发模型是基于消息通信的并发模型的典型代表。它使开发者无需处理锁带来的复杂问题,并且具有较高的并发度,因此得到了广泛的应用。当前 ArkTS 提供了 TaskPool 和 Worker 两种并发能力,两者均基于 Actor 并发模型实现。
Actor 并发模型每一个线程都是一个独立 Actor,每个 Actor 有自己独立的内存,Actor 之间通过消息传递机制触发对方 Actor 的行为,不同 Actor 之间不能直接访问对方的内存空间。
Actor 并发模型相较于内存共享并发模型,不同线程间的内存是隔离的,因此不会出现线程竞争同一内存资源的情况。开发者无需处理内存上锁相关的问题,从而提高开发效率。
下面示意图展示了如何使用内存共享模型解决生产者消费者问题

下面示例简单展示了如何使用基于 Actor 模型的 TaskPool 并发能力来解决生产者消费者问题。

Actor 并发模型中,线程不共享内存,需通过线程间通信机制传递任务和结果。非共享内存方式的线程类似于传统系统中进程的概念,独立内存,现成间交互有较高成本。
TaskPool 和 Worker 的作用是为应用程序提供一个多线程的运行环境,用于处理耗时的计算任务或其他密集型任务。可以避免任务阻塞宿主线程,从而提高系统性能和资源利用率。
TaskPool 和 Worker 的实现特点对比如下:
TaskPool 偏向独立任务维度,该任务在线程中执行,无需关注线程的生命周期,超长任务(大于 3 分钟且非长时任务)会被系统自动回收。而 Worker 偏向线程的维度,支持长时间占据线程执行,需要开发者主动管理线程生命周期。
常见开发场景及适用说明如下:
运行时间超过 3 分钟的任务(不包括 Promise 和 async/await 异步调用的耗时,如网络下载、文件读写等 I/O 任务的耗时):例如后台进行 1 小时的预测算法训练等 CPU 密集型任务,需要使用 Worker。
有关联的一系列同步任务:例如在一些需要创建、使用句柄的场景中,句柄每次创建都是不同的,该句柄需永久保存,保证使用该句柄进行操作,需要使用 Worker。
需要设置优先级的任务:例如图库直方图绘制场景,后台计算的直方图数据会用于前台界面的显示,影响用户体验,需要高优先级处理,需要使用 TaskPool。
需要频繁取消的任务:例如图库大图浏览场景,为提升体验,会同时缓存当前图片左右侧各 2 张图片,往一侧滑动跳到下一张图片时,要取消另一侧的一个缓存任务,需要使用 TaskPool。
大量或调度点分散的任务:例如大型应用的多个模块包含多个耗时任务,不方便使用 Worker 去做负载管理,推荐使用 TaskPool。场景示例可参考批量数据写数据库场景。
我们这里需要实现一个类似守护线程的线程,所以采用 worker 方式自行管理线程生命周期等。
Worker 实现守护线程
DevEco Studio 支持一键生成 Worker,在对应的{moduleName}目录下任意位置,单击鼠标右键 > New > Worker,即可自动生成 Worker 的模板文件及配置信息。这里我们创建 MsgSyncWorker:
IDE 创建的 Worker 默认实现了回调函数,主要需要处理 onmesssage 的注册,用来接收宿主线程的指令,onmessage 对应函数会执行在子线程,可以通过 workerPort.postMessage 给宿主线程发送消息。
接着在宿主线程中启动 worker:
遇到问题
因为 HarmonyOS 中线程间内存不共享,所以之前主线程初始化的一些数据库和网络单例类需要重新再初始化,但是在初始化数据库是遇到了崩溃:Error message: UserDaoHelper is not initialized。
UserDaoHelper 是一个单例,在执行 UserDaoHelper 的 getInstance 之前就崩溃了,UserDaoHelper 导入失败了。
问题原因分析
出现模块未初始化错误是因为模块间循环依赖引起。模块间循环依赖可能导致应用运行时模块依赖的变量未初始化,如下示例。index.ets 文件执行前,会先执行依赖的 page.ets 文件,page.ets 文件执行时又循环依赖了 index.ets 导出的 foo 符号。此时 index.ets 文件未执行,foo 变量尚未完成初始化,会导致运行时异常。
解决方案
可以通过 DevEco Studio 中 Code Linter 检查工具识别应用代码中的循环依赖并进行代码重构,消除循环依赖影响。
首先在工程根目录下创建 code-linter.json5 配置文件,配置如下:
接着在工程管理窗口中鼠标选中工程根目录,右键选择 Code Linter > Full Linter 执行代码全量检查。![[HarmonyOS Next Worker 线程中模块未初始化异常处理实战-2.png]]最后根据检查结果,对应用代码中的循环依赖部分进行代码重构。
##鸿蒙核心技术 ##鸿蒙开发工具 ##DevEco Studio####社交 ##
版权声明: 本文为 InfoQ 作者【轻口味】的原创文章。
原文链接:【http://xie.infoq.cn/article/ae45004d5b4cbfd1b90868944】。文章转载请联系作者。
评论