napi_call_threadsafe_function 实现跨线程获取数据
前提:
napi_call_threadsafe_function
一般是在处理回调事件时才使用,在 native
中将回调事件传递给 JS
中,由 JS
根据这些回调事件进行后续的动作(音频的播放、暂停,UI 的显示场景等等)
这里的 native
一般是指 C/C++
层的回调,通常该回调会在自己的线程中处理事件,不会影响到 JS
主线程,如果该回调发生在主线程中,可能会出现死锁的问题
即 CallInJs
持有了锁,而 CallJs
无法获取锁,最后无法发送条件变量通知
实现:
需要最关键的两个东西:env
和 tsfn
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;
- 创建 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;
}
- 定义
CallJs
,该函数用于传递env
参数并执行lambda
,lambda
实际是由 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();
}
- 定义
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;
}
- 定义 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;
}
具体解析:
CallInJs
先把lambda
封装到CallJsContext
,然后调用napi_call_threadsafe_function(tsfn_, ctx, napi_tsfn_blocking)
。- 这会把
ctx
投递到JS
线程的任务队列。 - 当
JS
线程空闲时,N-API
会调用你注册的CallJs
静态方法,并传入napi_env
和ctx
。 CallJs
里会执行ctx->cb_(env)
;,也就是你传入的lambda
。- 所以,
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
回调真正执行完毕后再继续。
CallInJs
在worker
线程中被调用,创建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
,也可能返回,所以必须用条件变量保护。