为有牺牲多壮志,敢教日月换新天。

HarmonyOS:Node-API典型场景开发(2)

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤博客园地址:为敢技术(https://www.cnblogs.com/strengthen/ 
➤GitHub地址:https://github.com/strengthen
➤原文地址:https://www.cnblogs.com/strengthen/p/18504462
➤如果链接不是为敢技术的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

引言

在Native侧C/C++开发场景中,对于计算简单、应用侧主线程需要实时等待结果的情况下,开发者往往会采用常见的同步方式开发业务逻辑。然而,在计算密集型场景中,对于需要执行耗时操作的逻辑,为了避免阻塞应用侧主线程,确保应用程序的性能和响应效率,开发者需要将该部分业务逻辑设计在Native侧进行异步执行。在异步开发中,开发者需要将C/C++子线程异步处理的结果反馈到ArkTS主线程,用以应用侧UI界面刷新。因此,本文将以开发案例为线索贯穿全文,来详细讲解同步开发、callback异步模型开发、promise异步模型开发以及线程安全开发等典型场景的机制原理和开发流程。

典型开发场景概述

使用Node-API进行同步任务开发

在同步任务开发中,ArkTS应用侧主线程将阻塞等待Native侧计算结果,Native侧计算结果通过方舟引擎反馈给ArkTS应用侧。此过程中,Native接口代码与ArkTS应用侧均运行在ArkTS主线程上。

比如,应用侧需要读取文件,同步模式下,应用侧程序将会一直等待Native侧文件读取完成,然后再继续运行。

使用Node-API进行异步任务开发

在异步任务开发中,应用侧在调用Native接口后,将会收到临时结果,并继续执行UI操作。Native侧将异步执行业务逻辑,不阻塞应用侧。

比如,应用侧需要读取文件,异步模式下,应用侧程序将不会等待Native侧文件读取完成,并继续运行。

 

使用Node-API进行线程安全开发

由于ArkTS天然线程安全,而Native侧代码需要开发者自行保障线程安全。Native侧C++子线程不可跨线程直接访问ArkTS对象。为此,Node-API提供了可保障线程异步执行与通信安全的机制 —— 线程安全函数。

比如,应用侧需要读取文件,使用线程安全进行实现时,应用侧程序将不会等待Native侧文件读取完成,并继续运行。

 

开发案例概述

本文将以一个图片加载的案例为背景来详细讲解上述几种典型场景的开发步骤和关键API使用方法。在此案例中,用户将通过UI界面上呈现的按钮分别选择对应的典型场景接口进行图片加载。

案例设计思路

本案例的实现分为ArkTS应用侧和Native侧两个部分,如下图所示:

ArkTS应用侧

提供多种典型场景接口按钮,以供用户进行场景选择。分别为同步调用、callback异步调用、promise异步调用、tsf(线程安全函数)异步调用以及错误图片。不同的按钮触发后,将会调用相应的Native接口进行同步或者异步处理,获取图片路径信息,从而刷新UI界面。其中,错误图片按钮反馈的是一个错误图片路径,触发后,界面上将会弹出一个错误提示弹窗。

Native侧

根据应用侧触发的不同场景,将会执行不同的接口实现。整个业务逻辑分为以下两个部分:

  • Native接口,该部分主要实现Native接口的同步处理或者异步处理逻辑。
    • 同步处理,Native接口将直接调用图片路径搜索功能,将应用侧传入的图片名称输入到生产者-消费者模型中,进行相应图片路径获取,最后反馈到ArkTS应用侧。
    • 异步处理,Native接口将通过异步工作项或者线程安全函数进行异步任务创建,将应用侧传入的图片名称输入到上下文数据对象中,待后续异步任务调度处理。在异步任务处理中,同样调度生产者-消费者模型进行图片路径获取,最后通过线程通信将结果反馈到ArkTS应用侧。
  • 生产者-消费者模型,该部分逻辑主要通过C++子线程、信号量以及条件变量等关键特性来实现。
    • 生产者线程负责根据图片名称搜索目标图片路径。并且将搜索结果放入缓冲队列中。
    • 消费者线程负责从缓冲队列中取出目标图片路径,并通过线程通信的方式将结果反馈到ArkTS应用侧。

案例流程图

案例效果图

生产者-消费者模型实现

(1)模型缓冲队列

ProducerConsumer.h 头文件

#ifndef MultiThreads_ProducerConsumer_H
#define MultiThreads_ProducerConsumer_H

#include <string>
#include <queue>
#include <mutex>
#include <condition_variable>

using namespace std;
// Principles of the producer-consumer model: Use buffer zones to balance the rate between production and consumption.
// Synchronization relationship 1: When the buffer is full, the producer needs to be blocked and wait. When a product
// pops up the buffer, the producer needs to be woken up for consumption. Synchronization relationship 2: When the
// buffer is empty, the consumer needs to be blocked and waited. When a product enters the buffer, the consumer needs to
// be woken up for consumption.
class ProducerConsumerQueue {
public:
    // constructor
    ProducerConsumerQueue() {}
    ProducerConsumerQueue(int queueSize) : m_maxSize(queueSize) {}
    // producer enqueue operation
    void PutElement(string element);
    // consumer dequeue operation
    string TakeElement();
private:
    // check whether the buffer queue is full
    bool isFull() { return (m_queue.size() == m_maxSize); }
    // check whether the buffer queue is empty
    bool isEmpty() { return m_queue.empty(); }
private:
    queue<string> m_queue{};         // buffer queue
    int m_maxSize{};                 // buffer queue capacity
    mutex m_mutex{};                 // the mutex is used to protect data consistency
    condition_variable m_notEmpty{}; // condition variable, which is used to indicate whether the buffer queue is empty
    condition_variable m_notFull{};  // condition variable, which is used to indicate whether the buffer queue is full
};
#endif // MultiThreads_ProducerConsumer_H

ProducerConsumer.cpp 源文件

#include "ProducerConsumer.h"

void ProducerConsumerQueue::PutElement(string element) {
    unique_lock<mutex> lock(m_mutex); // add mutex

    while (isFull()) {
        // when the data buffer queue is full, the production is blocked and wakes up after consumer consumption
        // m_mutex is automatically released at the same time
        m_notFull.wait(lock);
    }
    // reacquire the lock
    // if the queue is not full
    // the product is added to the queue and the consumer is notified that the product can be consumed
    m_queue.push(element);
    m_notEmpty.notify_one();
}
string ProducerConsumerQueue::TakeElement() {
    unique_lock<mutex> lock(m_mutex); // add mutex

    while (isEmpty()) {
        // when the data buffer queue is empty, the consumption is blocked and the producer is woken up after production
        // m_mutex is automatically released at the same time
        m_notEmpty.wait(lock);
    }
    // reacquire the lock
    // if the queue is not empty, the product is ejected and the producer is notified that it is ready for production
    string element = m_queue.front();
    m_queue.pop();
    m_notFull.notify_one();
    return element;
}

(2)全局变量及搜索接口定义

定义一个静态全局的模型缓冲队列。由于,每次只处理一张图片,所以将容量设定为1。

定义一个静态全局的图片路径集合,用于后文搜索。

MultiThreads.cpp文件

// define the buffer queue and set the capacity to 1
static ProducerConsumerQueue buffQueue(1);
// defines the image path set, which is used for later search
static vector<string> imagePathVec{"sync.png", "callback.png", "promise.png", "tsf.png"};
// check the image paths based on the image name
static bool CheckImagePath(const string &imageName, const string &imagePath) {
    // separate character strings by suffix
    size_t pos = imagePath.find_first_of('.');
    if (pos == string::npos) {
        return false;
    }
    string nameTemp = imagePath.substr(0, pos);
    if (nameTemp.empty()) {
        return false;
    }
    // determine whether the image path is the target path based on whether the image names are the same
    return (imageName == nameTemp);
}
// search for image paths by image name
static string SearchImagePath(const string &imageName) {
    for (const string &imagePath : imagePathVec) {
        if (CheckImagePath(imageName, imagePath)) {
            return imagePath;
        }
    }
    return string("");
}

(3)生产者线程执行函数

生产者线程的执行功能:通过解析上下文数据中的图片名称参数,来搜索相应的图片路径,并将其置入模型缓冲队列中。

MultiThreads.cpp文件

static void ProductElement(void *data) {
    buffQueue.PutElement(SearchImagePath(static_cast<ContextData *>(data)->args));
}

(4)消费者线程执行函数

消费者线程的执行功能:通过从模型缓冲队列中获取图片路径的搜索结果,并将其置入上下文数据中,用以后续反馈给ArkTS应用侧。

MultiThreads.cpp文件

非线程安全函数消费者执行函数:

static void ConsumeElement(void *data) { static_cast<ContextData *>(data)->result = buffQueue.TakeElement(); }

线程安全函数消费者执行函数:

static void ConsumeElementTSF(void *data) {
    static_cast<ContextData *>(data)->result = buffQueue.TakeElement();
    // bind consumer thread to the thread safe function
    (void)napi_acquire_threadsafe_function(tsFun);
    // send async task to the JS main thread EventLoop
    (void)napi_call_threadsafe_function(tsFun, data, napi_tsfn_blocking);
    // release thread reference
    (void)napi_release_threadsafe_function(tsFun, napi_tsfn_release);
}

使用Node-API进行同步任务开发

在同步任务开发中,ArkTS应用侧主线程将阻塞等待Native侧计算结果,Native侧计算结果通过方舟引擎反馈给ArkTS应用侧。此过程中,Native接口代码与ArkTS应用侧均运行在ArkTS主线程上。为了Native侧业务处理具有同步效果,案例中的生产者与消费者线程则采用join()的方式来进行同步处理。

同步调用也支持带Callback,具体方式由应用开发者决定,通过是否传递Callback函数进行区分。

同步任务开发时序交互图

从上图中,可以看出,当Native同步任务接口被调用时,该接口会完成参数解析、数据类型转换、创建生产者和消费者线程等。在等待生产者线程和消费者线程执行结束后,将图片路径结果转换为napi_value,由方舟引擎直接反馈到ArkTS应用侧。

同步任务开发步骤

(1)ArkTS应用侧开发

用户在界面点击“多线程同步调用”按钮,触发onClick()回调。在回调处理中,将会调用Native接口进行图片路径获取,以刷新UI界面。

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 sync call button
        Button($r('app.string.sync_button_title'))
          .width(Constants.FULL_PARENT)
          .margin($r('app.float.button_common_margin'))
          .onClick(() => {
            this.imageName = Constants.SYNC_BUTTON_IMAGE;
            this.imagePath = Constants.IMAGE_ROOT_PATH + testNapi.getImagePathSync(this.imageName);
          })
      ...
      }
      ...
    }
    ...
  }
}

(2)Native侧开发

在同步任务开发中,Native侧接口原生代码仍然运行在ArkTS主线程上。

导出Native接口:将Native接口导出到ArkTS侧,用于支撑ArkTS对象调用和模块编译构建。

index.d.ts文件

// export sync interface
export const getImagePathSync: (imageName: string) => string;

上下文数据结构定义:用于任务执行过程中,上下文数据存储。后文异步任务开发和线程安全开发中实现相同,后续不再赘述。

MultiThreads.cpp文件

// context data provided by users
// data is transferred between the native method (initialization data), ExecuteFunc, and CompleteFunc
struct ContextData {
    napi_async_work asyncWork = nullptr; // async work object
    napi_deferred deferred = nullptr;    // associated object of the delay object promise
    napi_ref callbackRef = nullptr;      // reference of callback
    string args = "";                    // parameters from ArkTS --- imageName
    string result = "";                  // C++ sub-thread calculation result --- imagePath
};

销毁上下文数据:在任务执行结束或者失败后,需要销毁上下文数据进行内存释放。后文异步任务开发和线程安全开发中实现相同,后续不再赘述。

MultiThreads.cpp文件

static void DeleteContext(napi_env env, ContextData *contextData) {
    // delete callback reference
    if (contextData->callbackRef != nullptr) {
        (void)napi_delete_reference(env, contextData->callbackRef);
    }
    // delete async work
    if (contextData->asyncWork != nullptr) {
        (void)napi_delete_async_work(env, contextData->asyncWork);
    }
    // release context data
    delete contextData;
}

Native同步任务开发接口:解析ArkTS应用侧参数、数据类型转换、创建生产者和消费者线程,并使用join()进行同步处理。最后在获取结果后,将数据转换为napi_value类型,返回给ArkTS应用侧。

MultiThreads.cpp文件

// sync interface
static napi_value GetImagePathSync(napi_env env, napi_callback_info info) {
    size_t paraNum = 1;
    napi_value paraArray[1] = {nullptr};
    // parse parameters
    napi_status operStatus = napi_get_cb_info(env, info, &paraNum, paraArray, nullptr, nullptr);
    if (operStatus != napi_ok) {
        return nullptr;
    }
    napi_valuetype paraDataType = napi_undefined;
    operStatus = napi_typeof(env, paraArray[0], &paraDataType);
    if ((operStatus != napi_ok) || (paraDataType != napi_string)) {
        return nullptr;
    }
    // convert napi_value 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;
    }
    // defines context data. the memory will be released in CompleteFunc
    auto contextData = new ContextData;
    contextData->args = strBuff;
    // create producer thread
    thread producer(ProductElement, static_cast<void *>(contextData));
    producer.join();
    // create consumer thread
    thread consumer(ConsumeElement, static_cast<void *>(contextData));
    consumer.join();
    // convert the result to napi_value and send it to ArkTs application
    napi_value result = nullptr;
    (void)napi_create_string_utf8(env, contextData->result.c_str(), contextData->result.length(), &result);
    // delete context data
    DeleteContext(env, contextData);
    return result;
}

使用Node-API进行异步任务开发

Node-API异步任务机制概述

Node-API异步任务开发主要用于执行耗时操作的场景中使用,以避免阻塞主线程,确保应用程序的性能和响应效率。例如以下场景:

  • 文件操作:读取大型文件或执行复杂的文件操作时,可以使用异步工作项来避免阻塞主线程。
  • 网络请求:当需要进行网络请求并等待响应时,可以使用异步工作项来避免阻塞主线程,从而提高应用程序的响应性能。
  • 数据库操作:当需要执行复杂的数据库查询或写入操作时,可以使用异步工作项来避免阻塞主线程,从而提高应用程序的并发性能。
  • 图形处理:当需要对大型图像进行处理或执行复杂的图像算法时,可以使用异步工作项来避免阻塞主线程,从而提高应用程序的实时性能。

异步方式与同步方式的区别在于,同步方式中所有代码的处理都在ArkTS主线程中完成,而异步方式中的所有代码在多线程中完成。Node-API主要是通过创建一个异步工作项来实现异步任务开发。总体步骤如下:

  1. 在Native接口函数中,创建一个异步工作项,并置入libuv调度队列中,然后立即返回一个临时结果给ArkTS调用者;
  2. 通过libuv线程池创建并调度work子线程完成异步业务逻辑的执行;
  3. 通过Callback回调或者Promise延时对象返回真正的处理结果,并用于应用侧UI刷新。

异步工作项的底层机制是基于libuv异步库来实现的,具体流程原理如下:

异步方式依赖Node-API提供的napi_create_async_work接口创建异步工作项:

NAPI_EXTERN napi_status napi_create_async_work(napi_env env,
                                               napi_value async_resource,
                                               napi_value async_resource_name,
                                               napi_async_execute_callback execute,
                                               napi_async_complete_callback complete,
                                               void* data,
                                               napi_async_work* result);
参数说明:
[in] env:传入接口调用者的环境,包含方舟引擎等。由框架提供,默认情况下直接传入即可。
[in] async_resource:可选项,关联async_hooks。
[in] async_resource_name:异步资源标识符,主要用于async_hooks API暴露断言诊断信息。
[in] execute:执行业务逻辑计算函数,由libuv线程池调度执行。在该函数中执行IO、CPU密集型任务,不阻塞主线程。
[in] complete:execute回调函数执行完成或取消后,触发执行该函数。此函数在EventLoop子线程中执行。
[in] data:用户提供的上下文数据,用于传递数据。
[out] result:napi_async_work*指针,用于返回当前此处函数调用创建的异步工作项。 返回值:返回napi_ok表示转换成功,其他值失败。

Execute回调

  • execute函数用于执行工作项的业务逻辑,异步工作项被调度后,该函数从上下文数据中获取输入数据,在work子线程中完成业务逻辑计算(不阻塞主线程)并将结果写入上下文数据。
  • 因为execute函数不在ArkTS线程中,所以不允许execute函数调用napi的接口。业务逻辑的返回值可以返回到complete回调中处理。

Complete回调

  • 业务逻辑处理execute函数执行完成或被取消后,通过事件通知EventLoop执行complete函数,complete函数从上下文数据中获取结果,转换为napi_value类型,调用ArkTS回调函数或通过Promise resolve()返回结果。
  • 该函数运行在ArkTS主线程下,因此可以调用napi的接口,将execute中的返回值封装成ArkTS对象返回。

Node-API异步接口实现支持Callback方式和Promise方式,具体使用哪种方式由应用开发者决定,通过是否传递callback函数进行区分。下文将对两种异步模型作简要介绍:

Callback异步模型

  • 用户在调用Native接口的时候,Native接口将异步执行任务,并临时返回空值给ArkTS应用侧。
  • 异步任务执行结果以参数的形式提供给用户注册的ArkTS回调函数,并通过napi_call_function将ArkTS回调函数进行调用执行以反馈结果到ArkTS应用侧。

Promise异步模型

  • 用户在调用Native接口的时候,Native接口将异步执行任务,并返回一个Promise对象给ArkTS应用侧。
  • Promise对象提供了API使得异步执行可以按照同步的流程表示出来,避免了层层嵌套的回调引用。
  • 异步任务执行结果以参数的形式提供给与ArkTS应用侧Promise对象关联的deferred对象,并通过napi_resolve_deferred将计算结果反馈到ArkTS应用侧。

异步任务开发时序交互图

(一)Callback异步开发时序交互

(2)Promise异步开发时序交互

从上图中,可以看出,当Native异步任务接口被调用时,该接口会完成参数解析、数据类型转换、异步工作项创建、异步任务压入调度队列以及返回空值(Callback方式)或者Promise对象(Promise方式)等。异步任务的调度、执行以及反馈计算结果,均由libuv线程池和EventLoop机制进行系统调度完成。

异步任务开发步骤

(一)Callback异步开发步骤

(1)ArkTS应用侧开发

用户在界面点击“多线程callback异步调用”按钮,触发onClick()回调。在回调处理中,将会调用Native接口进行图片路径获取,以刷新UI界面。

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 callback async button
        Button($r('app.string.async_callback_button_title'))
          .width(Constants.FULL_PARENT)
          .margin($r('app.float.button_common_margin'))
          .onClick(() => {
            this.imageName = Constants.CALLBACK_BUTTON_IMAGE;
            testNapi.getImagePathAsyncCallBack(this.imageName, (result: string) => {
              this.imagePath = Constants.IMAGE_ROOT_PATH + result;
            });
          })
      ...
      }
      ...
    }
    ...
  }
}

(2)Native侧开发

导出Native接口:将Native接口导出到ArkTS侧,用于支撑ArkTS对象调用和模块编译构建。

index.d.ts文件

// export async callback interface
export const getImagePathAsyncCallBack: (imageName: string, callBack: (result: string) => void) => void;

execute回调:定义异步工作项的第一个回调函数,该函数在work子线程中执行,处理具体的业务逻辑。

MultiThreads.cpp文件

static void ExecuteFunc([[maybe_unused]] napi_env env, void *data) {
    // create producer thread
    thread producer(ProductElement, data);
    // the producer and consumer threads must be synchronized
    // otherwise, the complete operation is triggered to communicate with the ArkTS after the executeFunc is complete
    // the result is unpredictable
    producer.join();
    // create consumer thread
    thread consumer(ConsumeElement, data);
    consumer.join();
}

complete回调:定义异步工作项的第二个回调函数,该函数在ArkTS主线程中执行,将结果传递给ArkTS侧。

MultiThreads.cpp文件

static void CompleteFuncCallBack(napi_env env, [[maybe_unused]] napi_status status, void *data) {
    // parse context data
    ContextData *contextData = static_cast<ContextData *>(data);
    napi_value callBack = nullptr;
    napi_status operStatus = napi_get_reference_value(env, contextData->callbackRef, &callBack);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
        return;
    }
    // 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;
    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_async_work创建异步工作项,并使用napi_queue_async_work将异步任务加入队列,等待调度执行。

MultiThreads.cpp文件

// callback async interface
static napi_value GetImagePathAsyncCallBack(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, &paraNum, paraArray, nullptr, nullptr);
    if (operStatus != napi_ok) {
        return nullptr;
    }
    napi_valuetype paraDataType = napi_undefined;
    operStatus = napi_typeof(env, paraArray[0], &paraDataType);
    if ((operStatus != napi_ok) || (paraDataType != napi_string)) {
        return nullptr;
    }
    operStatus = napi_typeof(env, paraArray[1], &paraDataType);
    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;
    }
    // defines context data. the memory will be released in CompleteFunc
    auto contextData = new ContextData;
    contextData->args = strBuff;
    operStatus = napi_create_reference(env, paraArray[1], 1, &contextData->callbackRef);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
        return nullptr;
    }
    // async resource
    napi_value asyncName = nullptr;
    string asyncStr = "async callback";
    operStatus = napi_create_string_utf8(env, asyncStr.c_str(), asyncStr.length(), &asyncName);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
        return nullptr;
    }
    // create async work
    operStatus = napi_create_async_work(env, nullptr, asyncName, ExecuteFunc, CompleteFuncCallBack,
                                        static_cast<void *>(contextData), &contextData->asyncWork);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
        return nullptr;
    }
    // add the async work to the queue and wait for scheduling
    operStatus = napi_queue_async_work(env, contextData->asyncWork);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
    }
    return nullptr;
}

(二)Promise异步开发步骤

(1)ArkTS应用侧开发

用户在界面点击“多线程promise异步调用”按钮,触发onClick()回调。在回调处理中,将会调用Native接口进行图片路径获取,以刷新UI界面。

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 promise async button
        Button($r('app.string.async_promise_button_title'))
          .width(Constants.FULL_PARENT)
          .margin($r('app.float.button_common_margin'))
          .onClick(() => {
            this.imageName = Constants.PROMISE_BUTTON_IMAGE;
            let promiseObj = testNapi.getImagePathAsyncPromise(this.imageName);
            promiseObj.then((result: string) => {
              this.imagePath = Constants.IMAGE_ROOT_PATH + result;
            })
          })
      ...
      }
      ...
    }
    ...
  }
}

(2)Native侧开发

导出Native接口:将Native接口导出到ArkTS侧,用于支撑ArkTS对象调用和模块编译构建。

index.d.ts文件

// export async promise interface
export const getImagePathAsyncPromise: (imageName: string) => Promise<string>;

execute回调:定义异步工作项的第一个回调函数,该函数在work子线程中执行,处理具体的业务逻辑。

MultiThreads.cpp文件

static void ExecuteFunc([[maybe_unused]] napi_env env, void *data) {
    // create producer thread
    thread producer(ProductElement, data);
    // the producer and consumer threads must be synchronized
    // otherwise, the complete operation is triggered to communicate with the ArkTS after the executeFunc is complete
    // the result is unpredictable
    producer.join();
    // create consumer thread
    thread consumer(ConsumeElement, data);
    consumer.join();
}

complete回调:定义异步工作项的第二个回调函数,该函数在ArkTS主线程中执行,将结果传递给ArkTS侧。

MultiThreads.cpp文件

static void CompleteFuncPromise(napi_env env, [[maybe_unused]] napi_status status, void *data) {
    // parse context data
    ContextData *contextData = static_cast<ContextData *>(data);
    // convert the calculation result of C++ sub-thread to the napi_value type
    napi_value promiseArgs = nullptr;
    napi_status operStatus =
        napi_create_string_utf8(env, contextData->result.c_str(), contextData->result.length(), &promiseArgs);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
        return;
    }
    // the deferred and promise object are associated. the result is sent to ArkTS application through this interface
    operStatus = napi_resolve_deferred(env, contextData->deferred, promiseArgs);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
        return;
    }
    // destroy data and release memory
    DeleteContext(env, contextData);
}

Native异步任务开发接口:解析ArkTS应用侧参数,使用napi_create_async_work创建异步工作项,并使用napi_queue_async_work将异步任务加入队列,等待调度执行。

MultiThreads.cpp文件

// promise async interface
static napi_value GetImagePathAsyncPromise(napi_env env, napi_callback_info info) {
    size_t paraNum = 1;
    napi_value paraArray[1] = {nullptr};
    // parse parameters
    napi_status operStatus = napi_get_cb_info(env, info, &paraNum, paraArray, nullptr, nullptr);
    if (operStatus != napi_ok) {
        return nullptr;
    }
    napi_valuetype paraDataType = napi_undefined;
    operStatus = napi_typeof(env, paraArray[0], &paraDataType);
    if ((operStatus != napi_ok) || (paraDataType != napi_string)) {
        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;
    }
    // defines context data. the memory will be released in CompleteFunc
    auto contextData = new ContextData;
    contextData->args = strBuff;
    // async resource
    napi_value asyncName = nullptr;
    string asyncStr = "async promise";
    operStatus = napi_create_string_utf8(env, asyncStr.c_str(), asyncStr.length(), &asyncName);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
        return nullptr;
    }
    // create async work
    operStatus = napi_create_async_work(env, nullptr, asyncName, ExecuteFunc, CompleteFuncPromise,
                                        static_cast<void *>(contextData), &contextData->asyncWork);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
        return nullptr;
    }
    // add the async work to the queue and wait for scheduling
    operStatus = napi_queue_async_work(env, contextData->asyncWork);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
        return nullptr;
    }
    // create promise object
    napi_value promiseObj = nullptr;
    operStatus = napi_create_promise(env, &contextData->deferred, &promiseObj);
    if (operStatus != napi_ok) {
        DeleteContext(env, contextData);
        return nullptr;
    }
    return promiseObj;
}
使用Node-API进行线程安全开发

Node-API线程安全机制概述

Node-API线程安全开发主要用于异步多线程之间共享和调用场景中使用,以避免出现竞争条件或死锁。例如以下场景:

  • 异步计算:如果需要进行耗时的计算或IO操作,可以创建一个线程安全函数,将计算或IO操作放在另一个线程中执行,避免阻塞主线程,提高程序的响应速度。
  • 数据共享:如果多个线程需要访问同一份数据,可以创建一个线程安全函数,确保数据的读写操作不会发生竞争条件或死锁等问题。
  • 多线程开发:如果需要进行多线程开发,可以创建一个线程安全函数,确保多个线程之间的通信和同步操作正确无误。

Node-API接口只能在ArkTS主线程上进行调用。当C++子线程或者work子线程需要调用ArkTS回调接口或者Node-API接口时,这些线程是需要与ArkTS主线程进行通信才能完成的。Node-API提供了类型napi_threadsafe_function(线程安全函数)以及创建、销毁和调用该类型对象的 API来完成此操作。Node-API主要是通过创建一个线程安全函数,然后在C++子线程或者work子线程中调用线程安全函数来实现线程安全开发。总体步骤如下:

  1. 在Native接口函数中,创建一个线程安全函数对象,并注册绑定ArkTS回调接口callback和线程安全回调函数call_js_cb,然后立即返回一个临时结果给ArkTS调用者;
  2. 通过系统调度C++子线程完成异步业务逻辑的执行,并在子线程的执行函数中调用napi_call_threadsafe_function,将call_js_cb抛给EventLoop事件循环进行调度;
  3. 通过call_js_cb执行,调用napi_call_function调用ArkTS回调接口callback,从而将异步计算结果反馈到ArkTS应用侧,用于应用侧UI刷新。

线程安全函数的底层机制依然是基于libuv异步库来实现的,其原理流程如下: 

线程安全函数机制中关键的两个接口napi_create_threadsafe_function()和napi_call_threadsafe_function()参数列表详解:

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:线程安全函数对象指针。

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应用侧开发

用户在界面点击“多线程tsf异步调用”按钮,触发onClick()回调。在回调处理中,将会调用Native接口进行图片路径获取,以刷新UI界面。

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 thread safe function interface
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, &paraNum, paraArray, nullptr, nullptr);
    if (operStatus != napi_ok) {
        return nullptr;
    }
    napi_valuetype paraDataType = napi_undefined;
    operStatus = napi_typeof(env, paraArray[0], &paraDataType);
    if ((operStatus != napi_ok) || (paraDataType != napi_string)) {
        return nullptr;
    }
    operStatus = napi_typeof(env, paraArray[1], &paraDataType);
    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;
}
参考链接:示例代码

posted @ 2024-10-26 20:38  为敢技术  阅读(0)  评论(0编辑  收藏  举报