ArkTS/JS与Native的多线程交互调用

 原创    2024-01-04

这是关于 HarmonyOS 的第一篇技术笔记,主要介绍使用基于 Node-API 的线程安全接口完成 ArkTS/JS与C/C++之间的多线程交互。

题外话

目前各大厂商都在进行 HarmonyOS NEXT 的适配,它是对鸿蒙操作系统具有重大意义的版本,将不再兼容 Android 应用,需要使用专用的开发工具(DevEcho-Studio)和开发语言(ArkTS)重新开发 App。

我已经进行 Harmony 系统开发半月有余,也接触到了搭载 HarmonyOS NEXT 的真机,坦率来讲各方面体验并不友好。感觉华为在 HarmonyOS NEXT 上有点操之过急了,操作系统、开发工具、SDK和文档都还有很多需要完善的地方,对接群里几乎每天都有 Bug 报告出来,先行进行适配的开发者算是充当了华为的小白鼠了。

ArkTS 和 Node-API

HarmonyOS 官方的开发语言为 ArkTS:

ArkTS在保持TypeScript(简称TS)基本语法风格的基础上,对TS的动态类型特性施加更严格的约束,引入静态类型。同时,提供了声明式UI、状态管理等相应的能力,让开发者可以以更简洁、更自然的方式开发高性能应用。

Node.js 是跨平台的 JavaScript 运行时环境,HarmonyOS 使用 Node-API 规范接口来提供ArkTS/JS与C/C++模块之间交互能力。

Node-API 的线程约束

主要有两条:

  • Node-API 接口只能在 JS 线程使用,也即由 JS 调用 Native 的线程才能使用 Node-API 接口,而 Native 的子线程无法使用。
  • napi_env 是与 JS 线程绑定的,无法跨线程使用。即使使用全局变量将 napi_env 保存,在其他线程使用也是会造成 Crash的。

以上约束实际上是一个意思,即涉及 Node-API 的操作必须在和 JS 的调用线程一致。

为什么会存在线程约束?

来自 ChatGPT 的回答:

主要因为V8引擎的限制。Node-API构建在V8引擎之上,而V8通常对于跨线程共享数据和并发执行的操作有一些限制。为了避免绕过V8引擎的限制,Node-API对线程调用进行了约束。

如何处理 Native 子线程需要调用 JS 方法的问题?

熟悉了 Objective-C/Swift 和 C/C++ 近乎丝滑无痕的交互之后,再来看 JS 和 Native 的交互简直就是灾难。因为 Node.js 运行环境的限制,在 Native 的子线程调用 JS 方法需要特殊处理,Node-API 通过将调用切换到 JS 线程的方式来规避 Node-API 的线程约束限制。具体如下。

1、在Native侧实现调用JS线程获取结果的方法

threadsafe API 可以确保调用到此函数的线程一定是JS线程。

static void hm_threadsafe_func(napi_env env, napi_value js_fun, void *context, void *data) {
    size_t argc = 1;
    napi_value argv[1];
    napi_create_int32(env, *data, &argv[0]);
    
    napi_value out_value = NULL;
    napi_status ret = napi_call_function(env, NULL, js_fun, argc, argv, &out_value);
}

2、获取 threadsafe 句柄

定义暴露给 JS 层调用的 hm_register_threadsafe_func 方法。使用 napi_create_threadsafe_function 创建线程安全函数,并获取函数的引用句柄,对该句柄在多线程环境中的调用会异步切换到 JS 线程,最终会在 hm_threadsafe_func 函数中返回结果。

napi_threadsafe_function g_threadsafe_func_handle = NULL;

napi_value hm_register_threadsafe_func(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1] = {NULL};
    napi_get_cb_info(env, info, &argc, args, NULL, NULL);//args[0] 为 TS 层传入的 function 引用
    
    if (true) {
        // 线程安全
        napi_status status = napi_create_threadsafe_function(env, args[0], NULL, "threadsafe_func", 0, 1, NULL, NULL, NULL, hm_threadsafe_func, &g_threadsafe_func_handle);
    } else {
        // 非线程安全
        napi_create_reference(env, args[0], 1, &g_func_ref);
    }
    
    return NULL;
}

3、在 Native 子线程中安全调用

void *data = null;//业务数据
napi_status status = napi_call_threadsafe_function(g_threadsafe_func_handle, data, napi_tsfn_blocking);

napi_call_threadsafe_function将异步调用,调用结果将在 JS 线程中通过 hm_threadsafe_func 方法获取。

如何在子线程中同步获取结果?

上述方案实现了在子线程中调用 JS 代码,实际上只是将调用切换到主线程调用,最终结果将在主线程异步返回。那么,如何在子线程同步获取结果呢?

很遗憾,我没有找到很优雅的实现方案,即使通过 napi_create_async_work 相关接口也都是异步回调。

// C/C++ 子线程中的异步任务。执行耗时的操作,将结果传递给JavaScript层
void MyAsyncTask(napi_env env, napi_value callback) {
  napi_async_work asyncWork;
  napi_create_async_work(env, NULL, "MyAsyncTask", MyAsyncTaskExecute,
                         MyAsyncTaskComplete, NULL, &asyncWork);
  napi_queue_async_work(env, asyncWork);
}

// 异步任务的执行函数,在 C/C++ 子线程中进行
void MyAsyncTaskExecute(napi_env env, void* data) {
}

// 异步任务完成时的回调函数,结果传递给 JavaScript 层
void MyAsyncTaskComplete(napi_env env, napi_status status, void* data) {
}

那么,如果有此类需求只好使用信号量等常规手段在子线程获取结果了。如果你有便捷的实现方案,欢迎交流学习。

相关文章:

HarmonyOS的包类型和 ohpm 包管理工具

发表留言

您的电子邮箱地址不会被公开,必填项已用*标注。发布的留言可能不会立即公开展示,请耐心等待审核通过。