使用Node-API进行线程安全开发
一、Node-API线程安全机制概述
Node-API线程安全开发主要用于异步多线程之间共享和调用场景中使用,以避免出现竞争条件或死锁。
1、适用场景
- 异步计算:如果需要进行耗时的计算或IO操作,可以创建一个线程安全函数,将计算或IO操作放在另一个线程中执行,避免阻塞主线程,提高程序的响应速度。
- 数据共享:如果多个线程需要访问同一份数据,可以创建一个线程安全函数,确保数据的读写操作不会发生竞争条件或死锁等问题。
- 多线程开发:如果需要进行多线程开发,可以创建一个线程安全函数,确保多个线程之间的通信和同步操作正确无误。
2、总体步骤
Node-API接口只能在ArkTS主线程上进行调用。当C++子线程或者work子线程需要调用ArkTS回调接口或者Node-API接口时,这些线程是需要与ArkTS主线程进行通信才能完成的。Node-API提供了类型napi_threadsafe_function(线程安全函数)以及创建、销毁和调用该类型对象的 API来完成此操作。Node-API主要是通过创建一个线程安全函数,然后在C++子线程或者work子线程中调用线程安全函数来实现线程安全开发。
步骤如下:
- 在Native接口函数中,创建一个线程安全函数对象,并注册绑定ArkTS回调接口callback和线程安全回调函数call_js_cb,然后立即返回一个临时结果给ArkTS调用者;
- 通过系统调度C++子线程完成异步业务逻辑的执行,并在子线程的执行函数中调用napi_call_threadsafe_function,将call_js_cb抛给EventLoop事件循环进行调度;
- 通过call_js_cb执行,调用napi_call_function调用ArkTS回调接口callback,从而将异步计算结果反馈到ArkTS应用侧,用于应用侧UI刷新。
3、原理流程
线程安全函数的底层机制依然是基于libuv异步库来实现的。
4、关键接口
线程安全函数机制中关键的两个接口napi_create_threadsafe_function()和napi_call_threadsafe_function()。
(1)napi_create_threadsafe_function
该接口主要用于创建线程安全对象,在创建的过程中,会注册异步过程中的关键信息:ArkTS侧回调接口callback、线程安全回调函数call_js_cb等。
NAPI_EXTERN napi_status napi_create_threadsafe_function(napi_env env,
napi_value func,
napi_value async_resource,
napi_value async_resource_name,
size_t max_queue_size,
size_t initial_thread_count,
void* thread_finalize_data,
napi_finalize thread_finalize_cb,
void* context,
napi_threadsafe_function_call_js call_js_cb,
napi_threadsafe_function* result);
参数说明:
[in] env:传入接口调用者的环境,包含方舟引擎等。由框架提供,默认情况下直接传入即可。
[in] func:ArkTS应用侧传入的回调接口callback,可为空。当该值为nullptr时,下文call_js_cb不能为nullptr。反之,亦然。两者不可同时为空。
[in] async_resource:关联async_hooks,可为空。
[in] async_resource_name:异步资源标识符,主要用于async_hooks API暴露断言诊断信息。
[in] max_queue_size:缓冲队列容量,0表示无限制。线程安全函数实现实质为生产者-消费者模型。
[in] initial_thread_count:初始线程,包括将使用此函数的主线程,可为空。
[in] thread_finalize_data:传递给thread_finalize_cb接口的参数,可为空。
[in] thread_finalize_cb:当线程安全函数结束释放时的回调接口,可为空。
[in] context:附加的上下文数据,可为空。
[in] call_js_cb:子线程需要处理的线程安全回调任务,类似于异步工作项中的complete回调。当调用napi_call_threadsafe_function后,被抛到ArkTS主线程EventLoop中,等待调度执行。当该值为空时,系统将会调用默认回调接口。
[out] result:线程安全函数对象指针。
(2)napi_call_threadsafe_function
该接口主要用于子线程中,将需要回到ArkTS主线程处理的任务抛到EventLoop中,等待调度执行。
NAPI_EXTERN napi_status napi_call_threadsafe_function(napi_threadsafe_function func, void* data, napi_threadsafe_function_call_mode is_blocking);
参数说明:
[in] func:线程安全函数对象。
[in] data:上述线程安全回调任务call_js_cb需要处理的数据。
[in] is_blocking:该参数控制接口是否以阻塞的方式运行。
如果参数为napi_tsfn_nonblocking,该接口将以非阻塞的方式运行。当缓冲队列已满,调用该接口后,则返回napi_queue_full错误码。
如果参数为napi_tsfn_blocking,该接口将以阻塞的方式运行,直到缓冲队列中有可用空间。
如果创建线程安全函数时,设置的最大队列容量为0,则napi_call_threadsafe_function()永远不会阻塞。
二、线程安全开发时序交互图
从上图中,可以看出,当Native线程安全异步接口被调用时,该接口会完成参数解析、数据类型转换、线程安全创建、在创建过程中的注册绑定ArkTS回调处理函数以及创建生产者和消费者线程等。异步任务的调度、执行以及反馈计算结果,均由C++子线程和EventLoop机制进行系统调度完成。
三、线程安全开发步骤(简介)
四、线程安全开发步骤(实例)
1、ArkTS应用侧开发
// Index.ets文件
import testNapi from 'libentry.so';
import Constants from '../../common/constants/CommonConstants';
@Entry
@Component
struct Index {
@State imagePath: string = Constants.INIT_IMAGE_PATH;
imageName: string = '';
build() {
Column() {
...
// button list, prompting the user to click the button to select the target image.
Column() {
...
// multi-threads tsf async button
Button($r('app.string.async_tsf_button_title'))
.width(Constants.FULL_PARENT)
.margin($r('app.float.button_common_margin'))
.onClick(() => {
this.imageName = Constants.TSF_BUTTON_IMAGE;
testNapi.getImagePathAsyncTSF(this.imageName, (result: string) => {
this.imagePath = Constants.IMAGE_ROOT_PATH + result;
});
})
...
}
...
}
...
}
}
2、Native侧开发
导出Native接口:将Native接口导出到ArkTS侧,用于支撑ArkTS对象调用和模块编译构建。
// index.d.ts文件
export const getImagePathAsyncTSF: (imageName: string, callBack: (result: string) => void) => void;
全局变量定义:定义线程安全函数对象、队列容量、初始化线程数。
// MultiThreads.cpp文件
// define global thread safe function
static napi_threadsafe_function tsFun = nullptr;
static constexpr int MAX_MSG_QUEUE_SIZE = 0; // indicates that the queue length is not limited
static constexpr int INITIAL_THREAD_COUNT = 1;
call_js_cb回调处理定义:定义消费者子线程中,通过napi_call_threadsafe_function抛到ArkTS主线程EventLoop中的回调处理函数。在该函数中,通过napi_call_function调用ArkTS应用侧传入的回调callback,将异步任务搜索到的图片路径结果反馈到ArkTS应用侧。
// MultiThreads.cpp文件
static void CallJsFunction(napi_env env, napi_value callBack, [[maybe_unused]] void *context, void *data) {
// parse context data
ContextData *contextData = static_cast<ContextData *>(data);
// define the undefined variable, which is used in napi_call_function
// because no other data is transferred, the variable is defined as undefined
napi_value undefined = nullptr;
napi_status operStatus = napi_get_undefined(env, &undefined);
if (operStatus != napi_ok) {
DeleteContext(env, contextData);
return;
}
// convert the calculation result of C++ sub-thread to the napi_value type
napi_value callBackArgs = nullptr;
operStatus = napi_create_string_utf8(env, contextData->result.c_str(), contextData->result.length(), &callBackArgs);
if (operStatus != napi_ok) {
DeleteContext(env, contextData);
return;
}
// call the JS callback and send the async calculation result on the Native to ArkTS application
napi_value callBackResult = nullptr;
(void)napi_call_function(env, undefined, callBack, 1, &callBackArgs, &callBackResult);
// destroy data and release memory
DeleteContext(env, contextData);
}
Native线程安全函数异步开发接口:解析ArkTS应用侧参数,使用napi_create_threadsafe_function创建线程安全函数对象,并在消费者线程执行函数ConsumeElementTSF中使用napi_call_threadsafe_function将异步回调处理函数call_js_cb抛到EventLoop中,等待调度执行。
// MultiThreads.cpp文件
// thread safe function async interface
static napi_value GetImagePathAsyncTSF(napi_env env, napi_callback_info info) {
size_t paraNum = 2;
napi_value paraArray[2] = {nullptr};
// parse parameters
napi_status operStatus = napi_get_cb_info(env, info, ¶Num, paraArray, nullptr, nullptr);
if (operStatus != napi_ok) {
return nullptr;
}
napi_valuetype paraDataType = napi_undefined;
operStatus = napi_typeof(env, paraArray[0], ¶DataType);
if ((operStatus != napi_ok) || (paraDataType != napi_string)) {
return nullptr;
}
operStatus = napi_typeof(env, paraArray[1], ¶DataType);
if ((operStatus != napi_ok) || (paraDataType != napi_function)) {
return nullptr;
}
// napi_value convert to char *
constexpr size_t buffSize = 100;
char strBuff[buffSize]{}; // char buffer for imageName string
size_t strLength = 0;
operStatus = napi_get_value_string_utf8(env, paraArray[0], strBuff, buffSize, &strLength);
if ((operStatus != napi_ok) || (strLength == 0)) {
return nullptr;
}
// async resource
napi_value asyncName = nullptr;
string asyncStr = "async napi_threadsafe_function";
operStatus = napi_create_string_utf8(env, asyncStr.c_str(), asyncStr.length(), &asyncName);
if (operStatus != napi_ok) {
return nullptr;
}
// defines context data. the memory will be released in CompleteFunc
auto contextData = new ContextData;
contextData->args = strBuff;
// create thread safe function
if (tsFun == nullptr) {
operStatus =
napi_create_threadsafe_function(env, paraArray[1], nullptr, asyncName, MAX_MSG_QUEUE_SIZE,
INITIAL_THREAD_COUNT, nullptr, nullptr, nullptr, CallJsFunction, &tsFun);
if (operStatus != napi_ok) {
DeleteContext(env, contextData);
return nullptr;
}
}
// create producer thread
thread producer(ProductElement, static_cast<void *>(contextData));
producer.detach(); // must be detached
// create consumer thread
thread consumer(ConsumeElementTSF, static_cast<void *>(contextData));
consumer.detach();
return nullptr;
}