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的实现特点对比如下:

实现TaskPoolWorker
内存模型线程间隔离,内存不共享。线程间隔离,内存不共享。
参数传递机制采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。

支持ArrayBuffer转移和SharedArrayBuffer共享。
采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。

支持ArrayBuffer转移和SharedArrayBuffer共享。
参数传递直接传递,无需封装,默认进行transfer。消息对象唯一参数,需要自己封装。
方法调用直接将方法传入调用。在Worker线程中解析消息并调用对应方法。
返回值异步调用后默认返回。主动发送消息,需在onmessage中解析并赋值。
生命周期TaskPool自行管理生命周期,无需关心任务负载高低。开发者自行管理Worker的数量及生命周期。
任务池个数上限自动管理,无需配置。同个进程下,最多支持同时开启64个Worker线程,实际数量由进程内存决定。
任务执行时长上限3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),长时任务无执行时长上限。无限制。
设置任务的优先级支持配置任务优先级。不支持。
执行任务的取消支持取消已经发起的任务。不支持。
线程复用支持。不支持。
任务延时执行支持。不支持。
设置任务依赖关系支持。不支持。
串行队列支持。不支持。
任务组支持。不支持。
由于TaskPool的工作线程会绑定系统的调度优先级,并支持负载均衡(自动扩缩容),相比之下,Worker需要开发者自行创建,存在创建耗时以及不支持设置调度优先级。因此,性能方面TaskPool优于Worker,官方推荐在大多数场景中使用TaskPool。

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:

// worker.ets
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';


const workerPort: ThreadWorkerGlobalScope = worker.workerPort;


// 注册onmessage回调,当Worker线程收到来自其宿主线程通过postMessage接口发送的消息时被调用,在Worker线程执行
workerPort.onmessage = (e: MessageEvents) => {
  let data: string = e.data;
  console.info('workerPort onmessage is: ', data);


  // 向主线程发送消息
  workerPort.postMessage('2');
}


// 注册onmessageerror回调,当Worker对象接收到一条无法被序列化的消息时被调用,在Worker线程执行
workerPort.onmessageerror = () => {
  console.info('workerPort onmessageerror');
}


// 注册onerror回调,当Worker在执行过程中发生异常被调用,在Worker线程执行
workerPort.onerror = (err: ErrorEvent) => {
  console.info('workerPort onerror err is: ', err.message);
}

IDE创建的Worker默认实现了回调函数,主要需要处理onmesssage的注册,用来接收宿主线程的指令,onmessage对应函数会执行在子线程,可以通过workerPort.postMessage给宿主线程发送消息。

接着在宿主线程中启动worker:

// 创建Worker对象
let workerInstance = new worker.ThreadWorker('entry/ets/workers/worker.ets');


// 注册onmessage回调,当宿主线程接收到来自其创建的Worker通过workerPort.postMessage接口发送的消息时被调用,在宿主线程执行
workerInstance.onmessage = (e: MessageEvents) => {
let data: string = e.data;
console.info("workerInstance onmessage is: ", data);
}

// 注册onerror回调,当Worker在执行过程中发生异常时被调用,在宿主线程执行
workerInstance.onerror = (err: ErrorEvent) => {
console.info("workerInstance onerror message is: " + err.message);
}


// 注册onmessageerror回调,当Worker对象接收到无法序列化的消息时被调用,在宿主线程执行
workerInstance.onmessageerror = () => {
console.info('workerInstance onmessageerror');
}


// 注册onexit回调,当Worker销毁时被调用,在宿主线程执行
workerInstance.onexit = (e: number) => {
// 如果Worker正常退出,code为0;如果异常退出,code为1
console.info("workerInstance onexit code is: ", e);
}


// 发送消息给Worker线程
workerInstance.postMessage('1');
遇到问题

因为HarmonyOS中线程间内存不共享,所以之前主线程初始化的一些数据库和网络单例类需要重新再初始化,但是在初始化数据库是遇到了崩溃:Error message: UserDaoHelper is not initialized。

UserDaoHelper是一个单例,在执行UserDaoHelper的getInstance之前就崩溃了,UserDaoHelper导入失败了。

问题原因分析

出现模块未初始化错误是因为模块间循环依赖引起。模块间循环依赖可能导致应用运行时模块依赖的变量未初始化,如下示例。index.ets文件执行前,会先执行依赖的page.ets文件,page.ets文件执行时又循环依赖了index.ets导出的foo符号。此时index.ets文件未执行,foo变量尚未完成初始化,会导致运行时异常。

// index.ets
import { bar } from './page'
export function foo() {
  bar()
}


// page.ets
import { foo } from './index'
export function bar() {
  foo()
}
bar()
解决方案

可以通过DevEco Studio中Code Linter检查工具识别应用代码中的循环依赖并进行代码重构,消除循环依赖影响。

首先在工程根目录下创建code-linter.json5配置文件,配置如下:

{
  "files": [ // 用于表示配置适用的文件范围的 glob 模式数组。
  "**/*.js",
  "**/*.ts",
  "**/*.ets"
  ],
  "rules": {
  "@security/no-cycle": "error" // 配置循环依赖检查规则。
  }
}

接着在工程管理窗口中鼠标选中工程根目录,右键选择Code Linter > Full Linter执行代码全量检查。
在这里插入图片描述

最后根据检查结果,对应用代码中的循环依赖部分进行代码重构。

##鸿蒙核心技术##鸿蒙开发工具##DevEco Studio##
##社交##

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

轻口味

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值