通過N-API使用Libuv線程池
本文轉(zhuǎn)載自微信公眾號(hào)「編程雜技」,作者theanarkh。轉(zhuǎn)載本文請聯(lián)系編程雜技公眾號(hào)。
Node.js不適合處理耗時(shí)操作是一直存在的問題,為此Node.js提供了三種解決方案。
1 子進(jìn)程
2 子線程
3 Libuv線程池
前兩種是開發(fā)效率比較高的,因?yàn)槲覀冎恍枰獙慾s。但是也有些缺點(diǎn)
1 執(zhí)行js的成本
2 雖然可以間接使用Libuv線程池,但是受限于Node.js提供的API。
3 無法利用c/c++層提供的解決方案(內(nèi)置或業(yè)界的)。
這時(shí)候我們可以嘗試第三種解決方案。直接通過N-API使用Libuv線程池。下面我們看看這么做。N-API提供了幾個(gè)API。
- napi_create_async_work // 創(chuàng)建一個(gè)worr,但是還沒有執(zhí)行
- napi_delete_async_work // 釋放上面創(chuàng)建的work的內(nèi)存
- napi_queue_async_work // 往Libuv提交一個(gè)work
- napi_cancel_async_work // 取消Libuv中的任務(wù),如果已經(jīng)在執(zhí)行則無法取消
接下來我們看看如何通過N-API使用Libuv線程池。首先看看js層。
- const { submitWork } = require('./build/Release/test.node');
- submitWork((sum) => {
- console.log(sum)
- })
js提交一個(gè)任務(wù),然后傳入一個(gè)回調(diào)。接著看看N-API的代碼。
- napi_value Init(napi_env env, napi_value exports) {
- napi_value func;
- napi_create_function(env,
- NULL,
- NAPI_AUTO_LENGTH,
- submitWork,
- NULL,
- &func);
- napi_set_named_property(env, exports, "submitWork", func);
- return exports;
- }
- NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
首先定義導(dǎo)出的函數(shù),接著看核心邏輯。
1 定義一個(gè)結(jié)構(gòu)體保存上下文
- struct info
- {
- int sum; // 保存計(jì)算結(jié)果
- napi_ref func; // 保存回調(diào)
- napi_async_work worker; // 保存work對(duì)象
- };
2 提交任務(wù)到Libuv
- static napi_value submitWork(napi_env env, napi_callback_info info) {
- napi_value resource_name;
- napi_status status;
- size_t argc = 1;
- napi_value args[1];
- struct info data = {0, nullptr, nullptr};
- struct info * ptr = &data;
- status = napi_get_cb_info(env, info, &argc, args, NULL, NULL);
- if (status != napi_ok) {
- goto done;
- }
- napi_create_reference(env, args[0], 1, &ptr->func);
- status = napi_create_string_utf8(env,"test", NAPI_AUTO_LENGTH, &resource_name);
- if (status != napi_ok) {
- goto done;
- }
- // 創(chuàng)建一個(gè)work,ptr保存的上下文會(huì)在work函數(shù)和done函數(shù)里使用
- status = napi_create_async_work(env, nullptr, resource_name, work, done, (void *) ptr, &ptr->worker);
- if (status != napi_ok) {
- goto done;
- }
- // 提及work到Libuv
- status = napi_queue_async_work(env, ptr->worker);
- done:
- napi_value ret;
- napi_create_int32(env, status == napi_ok ? 0 : -1, &ret);
- return ret;
- }
執(zhí)行上面的函數(shù),任務(wù)就會(huì)被提交到Libuv線程池了。
3 Libuv子線程執(zhí)行任務(wù)
- void work(napi_env env, void* data) {
- struct info *arg = (struct info *)data;
- printf("doing...\n");
- int sum = 0;
- for (int i = 0; i < 10; i++) {
- sum += i;
- }
- arg->sum = sum;
- }
很簡單,計(jì)算幾個(gè)數(shù)。并且保存結(jié)果。
4 回調(diào)js
- void done(napi_env env, napi_status status, void* data) {
- struct info *arg = (struct info *)data;
- if (status == napi_cancelled) {
- printf("cancel...");
- } else if (status == napi_ok) {
- printf("done...\n");
- napi_value callback;
- napi_value global;
- napi_value result;
- napi_value sum;
- // 拿到結(jié)果
- napi_create_int32(env, arg->sum, &sum);
- napi_get_reference_value(env, arg->func, &callback);
- napi_get_global(env, &global);
- // 回調(diào)js
- napi_call_function(env, global, callback, 1, &sum, &result);
- // 清理
- napi_delete_reference(env, arg->func);
- napi_delete_async_work(env, arg->worker);
- }
- }
并且執(zhí)行后,我們看到輸出了45。接下來我們分析大致的過程。首先我呢看看ThreadPoolWork,ThreadPoolWork是對(duì)Libuv work的封裝。
- class ThreadPoolWork {
- public:
- explicit inline ThreadPoolWork(Environment* env) : env_(env) {
- CHECK_NOT_NULL(env);
- }
- inline virtual ~ThreadPoolWork() = default;
- inline void ScheduleWork();
- inline int CancelWork();
- virtual void DoThreadPoolWork() = 0;
- virtual void AfterThreadPoolWork(int status) = 0;
- Environment* env() const { return env_; }
- private:
- Environment* env_;
- uv_work_t work_req_;
- };
類的定義很簡單,主要是封裝了uv_work_t。我們看看每個(gè)函數(shù)的意義。DoThreadPoolWork和AfterThreadPoolWork是虛函數(shù),由子類實(shí)現(xiàn),我們一會(huì)看子類的時(shí)候再分析。我們看看ScheduleWork
- void ThreadPoolWork::ScheduleWork() {
- env_->IncreaseWaitingRequestCounter();
- int status = uv_queue_work(
- env_->event_loop(),
- &work_req_,
- // Libuv子線程里執(zhí)行的任務(wù)函數(shù)
- [](uv_work_t* req) {
- ThreadPoolWork* self = ContainerOf(&ThreadPoolWork::work_req_, req);
- self->DoThreadPoolWork();
- },
- // 任務(wù)處理完后的回調(diào)
- [](uv_work_t* req, int status) {
- ThreadPoolWork* self = ContainerOf(&ThreadPoolWork::work_req_, req);
- self->env_->DecreaseWaitingRequestCounter();
- self->AfterThreadPoolWork(status);
- });
- CHECK_EQ(status, 0);
- }
ScheduleWork是負(fù)責(zé)給Libuv提交任務(wù)的函數(shù)。接著看看CancelWork。
- int ThreadPoolWork::CancelWork() {
- return uv_cancel(reinterpret_cast<uv_req_t*>(&work_req_));
- }
直接調(diào)用Libuv的函數(shù)取消任務(wù)??赐旮割?,我們看看子類的定義,子類在N-API里實(shí)現(xiàn)。
- class Work : public node::AsyncResource, public node::ThreadPoolWork {
- private:
- explicit Work(node_napi_env env,
- v8::Local<v8::Object> async_resource,
- v8::Local<v8::String> async_resource_name,
- napi_async_execute_callback execute,
- napi_async_complete_callback complete = nullptr,
- void* data = nullptr)
- : AsyncResource(env->isolate,
- async_resource,
- *v8::String::Utf8Value(env->isolate, async_resource_name)),
- ThreadPoolWork(env->node_env()),
- _env(env),
- _data(data),
- _execute(execute),
- _complete(complete) {
- }
- ~Work() override = default;
- public:
- static Work* New(node_napi_env env,
- v8::Local<v8::Object> async_resource,
- v8::Local<v8::String> async_resource_name,
- napi_async_execute_callback execute,
- napi_async_complete_callback complete,
- void* data) {
- return new Work(env, async_resource, async_resource_name,
- execute, complete, data);
- }
- // 釋放該類對(duì)象的內(nèi)存
- static void Delete(Work* work) {
- delete work;
- }
- // 執(zhí)行用戶設(shè)置的函數(shù)
- void DoThreadPoolWork() override {
- _execute(_env, _data);
- }
- void AfterThreadPoolWork(int status) override {
- // 執(zhí)行用戶設(shè)置的回調(diào)
- _complete(env, ConvertUVErrorCode(status), _data);
- }
- private:
- node_napi_env _env;
- // 用戶設(shè)置的數(shù)據(jù),用于保存執(zhí)行結(jié)果等
- void* _data;
- // 執(zhí)行任務(wù)的函數(shù)
- napi_async_execute_callback _execute;
- // 任務(wù)處理完的回調(diào)
- napi_async_complete_callback _complete;
- };
在Work類我們看到了虛函數(shù)DoThreadPoolWork和AfterThreadPoolWork的實(shí)現(xiàn),沒有太多邏輯。最后我們看看N-API提供的API的實(shí)現(xiàn)。
- 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) {
- v8::Local<v8::Context> context = env->context();
- v8::Local<v8::Object> resource;
- if (async_resource != nullptr) {
- CHECK_TO_OBJECT(env, context, resource, async_resource);
- } else {
- resource = v8::Object::New(env->isolate);
- }
- v8::Local<v8::String> resource_name;
- CHECK_TO_STRING(env, context, resource_name, async_resource_name);
- uvimpl::Work* work = uvimpl::Work::New(reinterpret_cast<node_napi_env>(env),
- resource,
- resource_name,
- execute,
- complete,
- data);
- *result = reinterpret_cast<napi_async_work>(work);
- return napi_clear_last_error(env);
- }
napi_create_async_work本質(zhì)上是對(duì)Work的簡單封裝,創(chuàng)建一個(gè)Work并返回給用戶。
2 napi_delete_async_work
- napi_status napi_delete_async_work(napi_env env, napi_async_work work) {
- CHECK_ENV(env);
- CHECK_ARG(env, work);
- uvimpl::Work::Delete(reinterpret_cast<uvimpl::Work*>(work));
- return napi_clear_last_error(env);
- }
napi_delete_async_work用于任務(wù)執(zhí)行完后釋放Work對(duì)應(yīng)的內(nèi)存。
3 napi_queue_async_work
- napi_status napi_queue_async_work(napi_env env, napi_async_work work) {
- CHECK_ENV(env);
- CHECK_ARG(env, work);
- napi_status status;
- uv_loop_t* event_loop = nullptr;
- status = napi_get_uv_event_loop(env, &event_loop);
- if (status != napi_ok)
- return napi_set_last_error(env, status);
- uvimpl::Work* w = reinterpret_cast<uvimpl::Work*>(work);
- w->ScheduleWork();
- return napi_clear_last_error(env);
- }
napi_queue_async_work是對(duì)ScheduleWork的封裝,作用是給Libuv線程池提交任務(wù)。
4 napi_cancel_async_work
- napi_status napi_cancel_async_work(napi_env env, napi_async_work work) {
- CHECK_ENV(env);
- CHECK_ARG(env, work);
- uvimpl::Work* w = reinterpret_cast<uvimpl::Work*>(work);
- CALL_UV(env, w->CancelWork());
- return napi_clear_last_error(env);
- }
napi_cancel_async_work是對(duì)CancelWork的封裝,即取消Libuv線程池的任務(wù)。我們看到一層層套,沒有太多邏輯,主要是要符合N-API的規(guī)范。
總結(jié):通過N-API提供的API,使得我們不再受限于Nod.js本身提供的一些異步接口(使用Libuv線程池的接口),而是直接使用Libuv線程池,這樣我們不僅可以自己寫c/c++,還可以復(fù)用業(yè)界的一些解決方案解決Node.js里的一些耗時(shí)任務(wù)。
倉庫:https://github.com/theanarkh/learn-to-write-nodejs-addons