napi_call_threadsafe_function 实现跨线程获取数据

前提:

napi_call_threadsafe_function 一般是在处理回调事件时才使用,在 native 中将回调事件传递给 JS 中,由 JS 根据这些回调事件进行后续的动作(音频的播放、暂停,UI 的显示场景等等)

这里的 native 一般是指 C/C++ 层的回调,通常该回调会在自己的线程中处理事件,不会影响到 JS 主线程,如果该回调发生在主线程中,可能会出现死锁的问题

CallInJs 持有了锁,而 CallJs 无法获取锁,最后无法发送条件变量通知

实现:

需要最关键的两个东西:envtsfn

env

env顾名思义,就是 JS 的运行环境,需要它的原因是在 napi_call_function 时需要该参数来调用 js 函数
tsfn 是线程安全函数,它的内部维护一个线程安全的队列,这样能确保数据按顺序传递,防止数据竞争和资源冲突,见:napi_create_async_work 和 napi_create_threadsafe_function 的使用场景分析

部分代码示例

结构体定义

typedef struct {
    std::function<void(napi_env)> cb_;
    bool called_;
    std::mutex lock_;
    std::condition_variable cv_;
} CallJsContext;
  1. 创建 tsfn
// Start
bool AppExecutor::Start(napi_env env) {
    std::unique_lock<std::mutex> lock(lock_);

    napi_status status;

    if (started_) {
        napi_throw_error(env, nullptr, "AppExecutor already started");
        return false;
    }

    napi_value work_name;
    // Create a string to describe this asynchronous operation.
    DCHECK(napi_create_string_utf8(env, "AppExecutor AsyncWork", NAPI_AUTO_LENGTH, &work_name));

    // Convert the callback retrieved from JavaScript into a thread-safe function
    // which we can call from a worker thread.
    DCHECK(napi_create_threadsafe_function(
        env, nullptr, nullptr, work_name, 0, 1, nullptr, nullptr, this, App::CallJs, &(tsfn_)));

    started_ = true;
    return true;
}
  1. 定义 CallJs,该函数用于传递 env 参数并执行 lambdalambda 实际是由 JS 线程处理
// This function is responsible for converting data coming in from the worker
// thread to napi_value items that can be passed into JavaScript, and for
// calling the JavaScript function.
void AppEngnExecutor::CallJs(napi_env env, napi_value js_cb, void * context, void * data) {
    CallJsContext * ctx = (CallJsContext *)data;
    ctx->cb_(env);
    {
        std::lock_guard<std::mutex> lock(ctx->lock_);
        ctx->called_ = true;
    }
    ctx->cv_.notify_one();
}
  1. 定义 CallInJs,该函数的作用是把 lambda 封装到 CallJsContext,然后调用 napi_call_threadsafe_function(tsfn_, ctx, napi_tsfn_blocking) 这会把 ctx 投递到 JS 线程的任务队列
void AppExecutor::CallInJs(std::function<void(napi_env)> cb) {
    napi_status status;
    CallJsContext * ctx = new CallJsContext;
    ctx->cb_ = cb;
    ctx->called_ = false;

    // Initiate the call into JavaScript. The call into JavaScript will not
    // have happened when this function returns, but it will be queued.
    DCHECK(napi_call_threadsafe_function(tsfn_, ctx, napi_tsfn_blocking));
    {
        std::unique_lock<std::mutex> lock(ctx->lock_);
        while (!ctx->called_)
            ctx->cv_.wait(lock);
    }
    delete ctx;
}
  1. 定义 native 的回调函数
int AppExecutor::NotifyMsg(void * pUser, int nMsgID, int nV1, int nV2, int nV3, void * pData) {
    AppExecutor* executor = (AppExecutor*)pUser;

    ```
    executor->CallInJs([&](napi_env env) {
        napi_status status;
        const size_t argc = 5;
        napi_value argv[argc] = {0};
        DCHECK(napi_create_int32(env, nMsgID, &argv[0]));
        DCHECK(napi_create_int32(env, nV1, &argv[1]));
        DCHECK(napi_create_int32(env, nV2, &argv[2]));
        DCHECK(napi_create_int32(env, nV3, &argv[3]));

        ```
        if (executor->cb_ref_ != nullptr) {
            napi_status status;
            napi_value jsthis;
            // 获取 jsthis
            DCHECK(napi_get_reference_value(env, executor->app_->GetRef(), &jsthis));
            napi_value value;
            // 获取 js 的回调函数引用
            DCHECK(napi_get_reference_value(env, executor->cb_ref_, &value));
            DCHECK(napi_call_function(env, jsthis, value, argc, argv, nullptr));
        }
    });

    return 0;
}

具体解析:

  1. CallInJs 先把 lambda 封装到 CallJsContext,然后调用 napi_call_threadsafe_function(tsfn_, ctx, napi_tsfn_blocking)
  2. 这会把 ctx 投递到 JS 线程的任务队列。
  3. JS 线程空闲时,N-API 会调用你注册的 CallJs 静态方法,并传入 napi_envctx
  4. CallJs 里会执行 ctx->cb_(env);,也就是你传入的 lambda
  5. 所以,lambda 的实际执行时机是 JS 线程处理到这个任务时,而不是在调用 CallInJs 的线程里。

这段解析的小结就是 CallInJs 里的 lambda 是异步投递到 JS 线程,由 N-API 框架在合适时机(通常很快)回调执行的。

流程分析

触发 native 回调函数 => CallInJs 将 lambda 封装到 CallJsContext 并将 ctx 投递到 JS 线程的任务队列 => 在 JS 线程中,由 N-API 调用 CallJs,并传入有效的 napi_env,执行 lambda,最终调用 Js 的回调函数 => Js 的回调函数被触发,在 JS 中处理这些事件(音频的播放、暂停,UI 的显示场景等等)

销毁 ref 和释放 tsfn

void AppEngnExecutor::Shutdown(napi_env env) {
    std::unique_lock<std::mutex> lock(lock_);

    if (started_) {
        WorkComplete();
    }

    if (cb_ref_ != nullptr) {
        napi_status status;
        DCHECK(napi_delete_reference(env, cb_ref_));
        cb_ref_ = nullptr;
    }
}

// This function runs on the main thread after `ExecuteWork` exits.
void AppEngnExecutor::WorkComplete() {
    napi_status status;

    // Clean up the thread-safe function.
    DCHECK(napi_release_threadsafe_function(tsfn_, napi_tsfn_release));

    // Set both values to nullptr so JavaScript can order a new run of the thread.
    tsfn_ = nullptr;
    started_ = false;
}

拓展:

问:分析 ctx->called_ 的作用,如果不使用 called_,会有问题吗?

答:ctx->called_ 的作用是同步:确保 CallInJs 里的调用线程在 JS 回调真正执行完毕后再继续。

  • CallInJsworker 线程中被调用,创建 CallJsContext,然后通过 napi_call_threadsafe_function 把 ctx 传递到主线程。
  • 主线程(或 JS 线程)会异步调用 CallJs,执行 ctx->cb_(env),然后加锁设置 ctx->called_ = true;,并 notify_one() 唤醒等待的 worker 线程。
  • CallInJs 里,worker 线程在 while (!ctx->called_) ctx->cv_.wait(lock); 处阻塞,直到 JS 回调执行完毕。

问:如果不使用 called_ 会怎样?

答:如果不使用 called_,即直接:

std::unique_lock<std::mutex> lock(ctx->lock_);
ctx->cv_.wait(lock);

  • 这样做会有竞态条件和虚假唤醒风险。
  • wait 可能在 notify_one 之前调用,也可能在之后调用。如果没有条件变量保护,线程可能永远不会被唤醒(丢失通知)。
  • 另外,std::condition_variable::wait 允许虚假唤醒,即使没有 notify_one,也可能返回,所以必须用条件变量保护。

相关:条件变量的虚假唤醒和唤醒丢失问题

posted @ 2025-06-05 11:53  strive-sun  阅读(146)  评论(0)    收藏  举报