• 作者:老汪软件技巧
  • 发表时间:2024-09-20 11:01
  • 浏览量:

1. 背景

ArkTS 层接口的异步如果不涉及 I/O 操作,则异步任务会在主线程的微任务执行时机触发,仍然占用主线程。推荐使用 TaskPool,分发到后台任务池进行。

就是这个回复,让我这个初学者开启了多线程异步任务的踩坑之旅。

2. 并发2.1 概述

并发是指在同一时间段内,能够处理多个任务的能力。HarmonyOS 系统提供了异步并发和多线程并发两种处理策略。

ArkTS 支持异步并发和多线程并发。

2.2 多线程并发之 TaskPool

并发模型是用来实现不同应用场景中并发任务的编程模型,常见的并发模型分为基于内存共享的并发模型和基于消息通信的并发模型。

Actor 并发模型作为基于消息通信并发模型的典型代表,不需要开发者去面对锁带来的一系列复杂偶发的问题,同时并发度也相对较高,因此得到了广泛的支持和使用。

当前 ArkTS 提供了 TaskPool 和 Worker 两种并发能力,TaskPool 和 Worker 都基于 Actor 并发模型实现。

PS:TaskPool 会随着应用进程起一个线程,省去了首次任务执行创建线程的开销,线程创建开销较小。

鸿蒙的多线程并发都是基于 Actor 并发模型实现,不是基于内存共享的。你不需要考虑对内存上锁导致的一系列功能、性能问题。但是 Actor 并发模型每一个线程都是一个独立 Actor,每个 Actor 有自己独立的内存,Actor 之间通过消息传递机制触发对方 Actor 的行为,不同 Actor 之间不能直接访问对方的内存空间。Actor 并发模型线程之间是内存隔离的。

3.TaskPool 开发流程(踩坑之旅)

好的,看完文档我就开始按照官方流程进行了如下代码编写:

使用@Concurrent 注解定义并发函数,在函数中执行 IO、计算等耗时操作。

//并发函数
@Concurrent
async function loadDnsTable(): Promise<Map<string, string>> {
  //TODO:读取raw下的资源文件,对齐进行解析并且缓存
}

2. Harmony 要求并发函数必须是全局 function 不能是类方法,???那我如何调用我自己创建的 RawTableReader 类的方法去读取、解析并且缓存路由表。故再次翻阅官方文档,终于在 FAQ 中找到了答案:话说你们就不能将他写在 taskpool 文档里么?!

export interface RawTableReader extends lang.ISendable {
  readRawTable(context: common.Context): Map<string, string>;
}

并发和线程__并发和线程安全

并发函数修改后如下:

@Concurrent
function loadDnsTable(args: Object[]): Map<string, string> {
  let rawTableReader: RawTableReader = args[0] as RawTableReader;
  //此处的context类是EntryAbility启动时后注入到VirtualDomain单例类中的
  let context: common.Context = VirtualDomain.getInstance().getAppContext();
  return rawTableReader.readRawTable(context);
}

3. 使用 TaskPool 执行包含密集 I/O 的并发函数:通过调用方法执行任务,并在回调中进行调度结果处理。

let task: taskpool.Task = new taskpool.Task('vdn', loadDnsTable, rawTableReader);
taskpool.execute(task).then((result: Object) => {
    let r: Map<string, string> = result as Map<string, string>;
  }).catch((error: BusinessError) => {
  VdnLog.warn(`loadDnsTable error code = ${error.code} message = ${error.message}`);
});

4. 好了开发完了,我开始了我的一次运行。不出意外报错了,断点调试半天大致意思是:context is undefined

难道单例类没初始化注入 context?检查代码以及断点再次尝试,EntryAbility 启动时已经注入了全局 context。好吧,那我直接在 context 获取地方进行断点。又是小半天过去,我发现了问题两次调用 VirtualDomain.getInstance()返回的实例竟然不是一个?! ! .我又思考并且到处翻阅文档好久,总算想起来了 Harmony 的多线程是基于 Actor 的内存隔离的不是内存共享的,我在主线程注入的 context 的 VirtualDomain 单例对象跟我子线程获取到的根本就不是一个,那肯定就 undefined 了。

5.我想起来之前华为的官方人员在 FAQ 中回复可以使用应用全局状态存储 AppStorage缓存 context 对象,于是我继续修改代码 context 改为使用官方全局单例 AppStorage 进行存储获取。结果是:再次失败,好了我用实践证明了官方的 AppStorage 在多线程情况下也是有问题的。大家使用时一定注意!

6.我就不信一个 context 我就解决不了了?再次查阅官方文档皇天不负苦心人,我再次找到了TaskPool 和 Worker 支持的序列化类型这篇文档里描述了 context 是 Native 绑定对象可以在 TaskPool 中进行序列化传输。因此再次修改代码

export calss xxx {
    ...
    let context: common.Context = VirtualDomain.getInstance().getAppContext();
    let task: taskpool.Task = new taskpool.Task('vdn', loadDnsTable, rawTableReader, context);
    ...
}
@Concurrent
function loadDnsTable(args: Object[]): Map<string, string> {
  let rawTableReader: RawTableReader = args[0] as RawTableReader;
  let context: common.Context = args[1] as common.Context;
  return rawTableReader.readRawTable(context);
}

7.这次代码直接报错了 Casting "Non-sendable" data to "Sendable" type is not allowed (arkts-sendable-as-expr)

我按照你的官方task api构建的 task,你也说了 context 是 Native 绑定对象是支持的序列化类型。结果 let ****context: common.Context = args[1] as common.Context; 你直接给我编译报错?

继续思考,进行了如下修改,编译 OK,运行测试也,我的踩坑之旅总算结束了。

//虽然入参是Object[]对象,这里的args要注意必须使用lang.ISendable[],否则就会编译报错
@Concurrent
function loadDnsTable(args: lang.ISendable[]): Map<string, string> {
  let rawTableReader: RawTableReader = args[0] as RawTableReader;
  let context: common.Context = args[1] as common.Context;
  return rawTableReader.readRawTable(context);
}

4. 总结4.1 技术经验4.2后续5. 团队介绍

「三翼鸟数字化技术平台-智家APP平台」通过持续迭代演进移动端一站式接入平台为三翼鸟APP、智家APP等多个APP提供基础运行框架、系统通用能力API、日志、网络访问、页面路由、动态化框架、UI组件库等移动端开发通用基础设施;通过Z·ONE平台为三翼鸟子领域提供项目管理和技术实践支撑能力,完成从代码托管、CI/CD系统、业务发布、线上实时监控等Devops与工程效能基础设施搭建。