目录

Implementing Communication Between Native Child Threads and the UI Main Thread

Overview

When you are developing on the native side, you may often encounter time-consuming tasks, such as I/O operations, domain name resolution, and complex calculations. If these tasks are directly executed in the main thread, the main thread will be severely blocked, affecting the normal process of subsequent tasks. As a result, the user interface response is delayed or even frame freezing occurs.

To improve code performance, such time-consuming tasks are usually executed in native child threads. Generally, a native child thread can independently complete its tasks. However, in most cases, data needs to be passed from the main thread to native child threads, or the execution result of the native child threads needs to be returned to the main thread.

In a multithreaded environment, a key issue is how to maintain safe communication between the background threads (native child threads) and the UI main thread. Generally, ArkTS functions can be called only in the main thread. If a child thread is created using std::thread or pthread on the native side, the context environment and data (napi_env, napi_value, and napi_ref) of the main thread cannot be directly used in the child thread context.

To ensure correctness, if ArkTS functions need to be called back after the calculation or processing is complete in a native child thread, the computation or processing result must be passed to the main thread using the thread synchronization mechanism. Then, the ArkTS functions can be safely called in the main thread environment. To implement safe communication between the UI main thread and native child threads, you can use the following solutions:

The thread-safe functions are recommended.

If thread-safe functions cannot meet development requirements, you can use the libuv library to customize a loop and use uv_async_send() to implement inter-thread communication.

The uv_queue_work() method in libuv can also implement inter-thread communication, but this method has the following drawbacks:

  • When uv_queue_work() is used for inter-thread communication, the execute callback generally implements an empty task without any maintenance and testing information. Once the asynchronous task is not called back, it is difficult to locate the fault. Using this method is inefficient and increases the difficulty in locating faults.
  • Using uv_queue_work() destroys the underlying data reliability. The uv_queue_work() function is designed to throw asynchronous tasks. After the execute callback of an asynchronous task is submitted to the thread pool, the task is scheduled for execution, which cannot ensure the execution sequence of the tasks submitted multiple times.

    For more information about the libuv library, see the API reference: libuv.

How to Implement

[h2]Using Thread-Safe Functions

HarmonyOS Node-API provides a series of APIs related to thread-safe functions. You can use these APIs to create a function object that can be shared and safely used among multiple threads on the native side. When creating the function object, you need to specify key information, such as the ArkTS callback function, asynchronous resource identifier, buffer queue capacity, and initial number of threads, which ensures the correctness and safety of the functions in a multithreaded environment. This information is used to ensure the correctness and security of the function in the multi-thread environment. With this mechanism, a child thread can pass data to the main thread, and the main thread calls the ArkTS callback to process the data.

ArkTS passes a callback function to native, and then creates a thread-safe function on native. The thread-safe function is bound to a callback function (this function is triggered when the thread-safe function is called using napi_call_threadsafe_function). Then, the context information and parameters to be used are saved, and the child thread is split (the child thread is bound to the context information and parameters to be used).

After system resources are allocated to a child thread on the native side, the child thread executes the corresponding service logic. The child thread calls the previously declared Node-API thread-safe function, which will be pushed to the event loop of the main thread for scheduling and execution.

Once scheduled in the event loop, the thread-safe function calls napi_call_back() to invoke the ArkTS callback function.

How to develop

  1. Pass a function from ArkTS to native.
  2. Create a thread-safe function in the main thread, save the context information, and create a child thread.
  3. Request and call the thread-safe function in a native child thread.
  4. Call the ArkTS callback function in the thread-safe function.

[h2]Using uv_async_send Method Based on libuv

The libuv library provides the uv_async_send() function to asynchronously send signals in a non-blocking event loop. This function allows signals to be sent between different threads or different parts of an event loop to trigger operations without calling the block function. uv_async_send() leverages the event loop and internal message queue to implement communication between threads. Specifically, it allows a thread (usually a worker thread) to send a signal to the event loop on the main thread to trigger a callback on the main thread. Therefore, you can define a callback function for calling an ArkTS function in the main thread. When the service logic in the child thread is executed, call uv_async_send() to instruct the main thread to invoke the callback function in the child thread. The callback invoked calls the ArkTS function.

Call process

First, ArkTS passes a callback function to the native side. After receiving the function, the native side saves the context information and parameters to be used later. Then, the napi_get_uv_event_loop() API is used to obtain the main thread loop, which will be executed in the main thread. Then, the async handle is initialized and the callback function to be called in the main thread is bound, and the loop is executed. Then, the child thread is split (the child is bound to the context information and parameters to be used).

Once allocated with system resources, the child thread on the native side calls uv_async_send() to instruct the main thread to call the function bound to async.

Upon receiving the signal, the loop running in the main thread calls the callback function bound to the async handle. Then, callback function invokes the ArkTS callback function.

How to develop

  1. Pass a function from ArkTS to native.
  2. Save context information on the native side.
  3. Use the napi_get_uv_event_loop() API to obtain the main thread loop and initialize the async handle to bind the callback function.
  4. In the child thread, call the uv_async_send() method to instruct the main thread to call the function bound to the async handle.
  5. Run the loop in the main thread to execute the function bound to async to trigger the ArkTS callback function.

Scenario Case

The following demonstrates how to use thread-safe functions and libuv to increase the value of an ArkTS variable by 30.

For the function reference passed from ArkTS to native, its lifecycle is limited to the scope defined. To ensure that this function reference can still be used after it exceeds its scope, you need to extend its lifecycle. You can call the napi_create_reference() API to create a reference for an ArkTS object. In this way, the object is prevented from being released in advance due to the garbage collection mechanism, thereby effectively prolonging the lifecycle of the object. However, when the reference is no longer used, remember to use napi_delete_reference() to release it. Otherwise, memory leaks may occur.

[h2]Using Thread-Safe Functions

  1. Pass a function from ArkTS to native.

    In ArkTS, define a callback function named work with the parameter param. In the function body, param is increased by 30. Finally, the function returns the updated param. This callback function is used as a parameter in threadSafeCase() on the native side.

    // Call Native side function and pass ArkTS side function to Native side.
    testNapi.threadSafeCase(this.work);
  2. Create a thread-safe function in the main thread, save the context information, and create a child thread.

    After receiving the callback function passed from ArkTS, the threadSafeCase() API creates a thread-safe function tsFn using napi_create_threadsafe_function(). tsFn then calls the ThreadSafeCallJs() method in the main thread, which in turn calls the callback function passed from ArkTS.

    When creating a child thread, save the context information and ArkTS function reference.

    napi_threadsafe_function tsFn;
    static int g_value = 0;
    
    struct CallbackContext {
        napi_env env = nullptr;
        napi_ref callbackRef = nullptr;
        uv_async_t *async = nullptr;
    };
    static napi_value ThreadSafeCase(napi_env env, napi_callback_info info) {
        size_t argc = 1;
        napi_value js_callback;
        napi_value workName;
        napi_get_cb_info(env, info, &argc, &js_callback, nullptr, nullptr);
    
        napi_valuetype valueType = napi_undefined;
        napi_typeof(env, js_callback, &valueType);
        if (valueType != napi_valuetype::napi_function) {
            return nullptr;
        }
    
        napi_create_string_utf8(env, "ThreadSafeCase", NAPI_AUTO_LENGTH, &workName);
        napi_create_threadsafe_function(env, nullptr, nullptr, workName, 0, 1, nullptr, nullptr, nullptr, ThreadSafeCallJs, &tsFn);
        auto asyncContext = new CallbackContext();
        asyncContext->env = env;
        napi_create_reference(env, js_callback, 1, &asyncContext->callbackRef);
        std::thread t(SubThread, asyncContext);
        t.detach();
        return nullptr;
    }
  3. Request and call the thread-safe function in a native child thread.

    In the child thread, call the thread-safe function tsFn using napi_call_threadsafe_function() and pass the CallbackContext structure data as a parameter to the thread-safe function. You can add service operations to a child thread as required.

    void SubThread(CallbackContext *asyncContext) {
        if (asyncContext == nullptr) {
            return;
        }
        napi_acquire_threadsafe_function(tsFn);
        OH_LOG_INFO(LOG_APP, "ThreadSafe ChildThread, value:[%{public}d]", g_value);
        napi_call_threadsafe_function(tsFn, asyncContext, napi_tsfn_nonblocking);
        napi_release_threadsafe_function(tsFn, napi_tsfn_release);
    }
  4. Call the ArkTS callback function in the thread-safe function.

    The ArkTS function is called to increase the value by 30.

    static void ThreadSafeCallJs(napi_env env, napi_value js_callBack, void *context, void *data) {
        CallbackContext *argContext = reinterpret_cast<CallbackContext *>(data);
        if (argContext != nullptr) {
            napi_get_reference_value(env, argContext->callbackRef, &js_callBack);
        } else {
            return;
        }
    
        napi_valuetype valueType = napi_undefined;
        napi_typeof(env, js_callBack, &valueType);
        if (valueType != napi_valuetype::napi_function) {
            napi_delete_reference(env, argContext->callbackRef);
            delete argContext;
            argContext = nullptr;
            return;
        }
    
        napi_value argv;
        napi_create_int32(env, g_value, &argv);
        napi_value result = nullptr;
        napi_call_function(env, nullptr, js_callBack, 1, &argv, &result);
        napi_get_value_int32(env, result, &g_value);
        OH_LOG_INFO(LOG_APP, "ThreadSafe CallArkTS end, value:[%{public}d]", g_value);
        napi_delete_reference(env, argContext->callbackRef);
        delete argContext;
        argContext = nullptr;
    }

[h2]Implemented based on the uv_async_send method of the libuv asynchronous library

To use the libuv library, you need to add libuv.so in the CMakeLists.txt file and import the header file uv.h in the code file that uses libuv APIs. In the following example, we import uv.h in the napi_init file.

// CMakeLists.txt file
target_link_libraries(entry PUBLIC libace_napi.z.so libhilog_ndk.z.so libuv.so)
// napi_init.cpp file
#include <uv.h>
  1. Pass a function from ArkTS to native.

    In ArkTS, define a callback function named work with the parameter param. In the function body, param is increased by 30. Finally, the function returns the updated param. This callback function is used as a parameter in libUvCase() on the native side.

    @State value: number = 0;
    work: Function = (param: number) => {
      param += 30;
      this.value = param;
      return param;
    }
    // Call Native side function and pass ArkTS side function to Native side.
    testNapi.libUvCase(this.work);
  2. Save context information on the native side.

    When libUvCase() receives the callback function passed from ArkTS, save the context information and ArkTS function reference.

    size_t argc = 1;
    napi_value callback_function;
    napi_get_cb_info(env, info, &argc, &callback_function, nullptr, nullptr);
    
    napi_valuetype valueType = napi_undefined;
    napi_typeof(env, callback_function, &valueType);
    if (valueType != napi_valuetype::napi_function) {
        return nullptr;
    }
    
    auto asyncContext = new CallbackContext();
    if (asyncContext == nullptr) {
        return nullptr;
    }
    asyncContext->env = env;
    napi_create_reference(env, callback_function, 1, &asyncContext->callbackRef);
  3. Use the napi_get_uv_event_loop() API to obtain the main thread loop and initialize the async handle to bind the callback function.

    After the data is saved in the libUvCase() API, the napi_get_uv_event_loop() API is used to obtain the main thread loop, which will run in the main thread. Initialize an async handle, which is bound to a callback function that runs in the main thread loop. (The ArkTS function will be called in this function later.) Then, create a child thread.

    uv_loop_t *loop = nullptr;
    if (napi_get_uv_event_loop(env, &loop) != napi_ok) {
        delete asyncContext;
        return nullptr;
    }
        
    uv_async_t *async = new uv_async_t();
    uv_async_init(loop, async, async_handler);
    async->data = asyncContext;
    asyncContext->async = async;
    
    std::thread caseThread(CallbackUvWorkTest, asyncContext);
    caseThread.detach();
    return nullptr;
  4. In the child thread, call the uv_async_send() method to instruct the main thread to call the function bound to the async handle.

    After the child thread obtains the system scheduling, call uv_async_send() to instruct the main thread to call the callback function bound to async.

    void CallbackUvWorkTest(CallbackContext *context) {
        if (context == nullptr) {
            return;
        }
        OH_LOG_INFO(LOG_APP, "LibUV ChildThread, value:[%{public}d]", g_value);
        uv_async_send(context->async);
    }
  5. Run the loop in the main thread to execute the function bound to the async handle, which triggers the ArkTS callback function.

    Upon receiving the signal passed by the child thread using uv_async_send(), the main thread calls the callback function bound to the async handle in the loop. This function runs in the main thread (that is, the obtained loop). Then, the ArkTS function is called to return a value increased by 30.

    void async_handler(uv_async_t *handle) {
        CallbackContext *context = static_cast<CallbackContext *>(handle->data);
        napi_handle_scope scope = nullptr;
        napi_open_handle_scope(context->env, &scope);
        if (scope == nullptr) {
            napi_delete_reference(context->env, context->callbackRef);
            delete context;
            context = nullptr;
            uv_close(reinterpret_cast<uv_handle_t*>(handle), [](uv_handle_t *handle) { delete reinterpret_cast<uv_async_t*>(handle); });
            return;
        }
        napi_value callback = nullptr;
        napi_get_reference_value(context->env, context->callbackRef, &callback);
        napi_value retArg;
        napi_create_int32(context->env, g_value, &retArg);
        napi_value result;
        napi_call_function(context->env, nullptr, callback, 1, &retArg, &result);
        napi_get_value_int32(context->env, result, &g_value);
        OH_LOG_INFO(LOG_APP, "LibUV CallArkTS end, value:[%{public}d]", g_value);
        napi_close_handle_scope(context->env, scope);
        napi_delete_reference(context->env, context->callbackRef);
        delete context;
        context = nullptr;
        uv_close(reinterpret_cast<uv_handle_t*>(handle), [](uv_handle_t *handle) { delete reinterpret_cast<uv_async_t*>(handle); });
    }

FAQ

[h2]How do I call ArkTS asynchronous methods on the native side and obtain the computation result asynchronously?

For details, see How do I call ArkTS asynchronous APIs on the native side and return asynchronous computation results to the native side? .

Sample Code