nodejs addon 结合 libuv 使用经验

April 20, 2016

最近在基于 electron 做客户端相关工作,需要写 nodejs 的 addon 以便拓展 electron 相关功能,涉及到一些高级东西,写下相关经验。

一:asynchronous addons

在开发过程中,我们一定会遇到需要在 c++中处理一些耗时的任务,这个时候,必须使用异步 addon,如果使用同步的就像下面的传统 addon 写法,就会导致 JavaScript 线程被 blocked。

// 引入 C++ addon
var addon = require("./cpp/build/Release/addon");
...
// 调用 addon, 这里就会被blocked住,直到c++返回结果
var outputdata = addon.get_some_result(some input data);

//do some thing

这种情况应该使用的是传统的 JavaScript 回调的机制:

addon.get_some_result_async(some input data, function(outputdata) {
  //do some thing
});

这时候就必须了解相关的的线程机制,在我们当前的 case 中会有两个线程,一个是 event loop【javascript 在其中执行的线程】,也是我们不想阻塞的线程。另一个线程是被 libuv 管理的线程 我们叫他 worker 线程,很显然你不能在 event loop 线程和 worker 线程共享各自的栈数据,所以我们需要某种方案在两个线程里面共享数据,以便 worker 线程能拿到 event loop 线程的输入,event loop 线程能拿到 worker 线程的输出。

整个调用流程如下图:

首先定义一个共享数据结构体,放在内存堆上。

struct Work {
  uv_work_t  request; //指向了work本身,libuv api 在启动worker 线程的时候接受一个uv_work_t类型的指针
/*
*Persistent代表存储在堆上,防止被回收,可以完成类似闭包的功能。
在这里要了解几个V8里面的概念
在v8当中所有JavaScript数据都是由GC管理的,内存分配都是在V8的堆中进行分配的,JavaScript的值和对象也都存放在V8的堆中。
这个堆由V8独立的去维护,失去引用的对象将会被V8的GC掉并可以重新分配给其他对象。而所有数据型(Value的派生类)变量的地址(Value*)本身就是一个Handle,即是对堆中对象的引用。
所以V8内存管理其实就是对Handle进行管理,这样通过Handle GC就能知道Heap中一个对象的引用情况,当一个对象的Handle引用为发生改变的时候,GC即可对该对象进行回收(gc)或者移动。而直接通过C++的方式去直接去引用一个对象,会使得该对象无法被V8管理。
另外Handle分为Local和Persistent两种:
Local是局部的,同时被HandleScope进行管理。
persistent,是全局的,不受HandleScope的管理,其作用域可以延伸到不同的函数可以用来实现一些Closure之类的效果。Persistent Handle对象需要Persistent::New, Persistent::Dispose配对使用,类似于C++中new和delete。

通常在一个函数中会有很多Handle,而HandleScope则相当于用来装Handle(Local)的容器,当HandleScope生命周期结束的时候,其内部包含的所有Handle也将会被释放,这将会引起堆中对象引用的更新。
HandleScope是分配在栈上,不能通过New的方式进行创建。对于同一个作用域内可以有多个HandleScope,新的HandleScope将会覆盖上一个HandleScope,并对Local Handle进行管理。
*/
  v8::Persistent<Function> callback; //callback存储javascript回调函数
  std::string input//例子中javascript传入的输入
  std::string output;//异步worker线程返回的输出
};

addon 的 get_some_result_async 方法,通过 libuv 启动 worker 线程

void get_some_result_async(const v8::FunctionCallbackInfo<v8::Value>&args) {
    Isolate* isolate = args.GetIsolate();
    Work * work = new Work();
    work->request.data = work;
    v8::String::Utf8Value param1(args[0]->ToString());
    std::string inputString = std::string(*param1);
    work->input = inputString;
     Local<Function> callback = Local<Function>::Cast(args[1]);
    work->callback.Reset(isolate, callback);
    // 启动 worker 线程异步去做一些事情,本线程中立即返回
    uv_queue_work(uv_default_loop(),&work->request,
        workAsync,workAsyncComplete);
    //直接返回给js了
    args.GetReturnValue().Set(Undefined(isolate));
}

worker 线程

static void workAsync(uv_work_t *req)
{
    Work *work = static_cast<Work *>(req->data);
    std:string input = work->input;
    //假装很耗时的样子
    sleep(3000);
}

worder 线程 complete 后回调,注意这个函数 执行在 v8 线程中

static void WorkAsyncComplete(uv_work_t *req,int status)
{
    Isolate * isolate = Isolate::GetCurrent();
    v8::HandleScope handleScope(isolate); // Required for Node 4.x

    Work *work = static_cast<Work *>(req->data);

    // 设置回调函数参数
    Handle<Value> argv[] = { v8::String::NewFromUtf8(isolate, work->output) };

    // 执行callback
    Local<Function>::New(isolate, work->callback)->
      Call(isolate->GetCurrentContext()->Global(), 1, argv);

   // 释放 persistent回调函数
    work->callback.Reset();

    delete work;
}