通过N-API使用Libuv线程池

系统 Windows
Node.js不适合处理耗时操作是一直存在的问题,为此Node.js提供了三种解决方案。

 [[403810]]

本文转载自微信公众号「编程杂技」,作者theanarkh。转载本文请联系编程杂技公众号。

Node.js不适合处理耗时操作是一直存在的问题,为此Node.js提供了三种解决方案。

1 子进程

2 子线程

3 Libuv线程池

前两种是开发效率比较高的,因为我们只需要写js。但是也有些缺点

1 执行js的成本

2 虽然可以间接使用Libuv线程池,但是受限于Node.js提供的API。

3 无法利用c/c++层提供的解决方案(内置或业界的)。

这时候我们可以尝试第三种解决方案。直接通过N-API使用Libuv线程池。下面我们看看这么做。N-API提供了几个API。

  1. napi_create_async_work // 创建一个worr,但是还没有执行 
  2. napi_delete_async_work // 释放上面创建的work的内存 
  3. napi_queue_async_work // 往Libuv提交一个work 
  4. napi_cancel_async_work // 取消Libuv中的任务,如果已经在执行则无法取消 

接下来我们看看如何通过N-API使用Libuv线程池。首先看看js层。

  1. const { submitWork } = require('./build/Release/test.node'); 
  2. submitWork((sum) => { 
  3.     console.log(sum
  4. }) 

js提交一个任务,然后传入一个回调。接着看看N-API的代码。

  1. napi_value Init(napi_env env, napi_value exports) { 
  2.   napi_value func; 
  3.   napi_create_function(env, 
  4.                       NULL
  5.                       NAPI_AUTO_LENGTH, 
  6.                       submitWork, 
  7.                       NULL
  8.                       &func); 
  9.   napi_set_named_property(env, exports, "submitWork", func); 
  10.   return exports; 
  11.  
  12. NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) 

首先定义导出的函数,接着看核心逻辑。

1 定义一个结构体保存上下文

  1. struct info 
  2.   int sum; // 保存计算结果 
  3.   napi_ref func; // 保存回调 
  4.   napi_async_work worker; // 保存work对象 
  5. }; 

2 提交任务到Libuv

  1. static napi_value submitWork(napi_env env, napi_callback_info info) { 
  2.   napi_value resource_name; 
  3.   napi_status status; 
  4.    
  5.   size_t argc = 1; 
  6.   napi_value args[1]; 
  7.   struct info data = {0, nullptr, nullptr}; 
  8.   struct info * ptr = &data; 
  9.   status = napi_get_cb_info(env, info, &argc, args, NULLNULL); 
  10.   if (status != napi_ok) { 
  11.     goto done; 
  12.   } 
  13.   napi_create_reference(env, args[0], 1, &ptr->func); 
  14.   status = napi_create_string_utf8(env,"test", NAPI_AUTO_LENGTH, &resource_name); 
  15.   if (status != napi_ok) { 
  16.     goto done; 
  17.   } 
  18.   // 创建一个work,ptr保存的上下文会在work函数和done函数里使用 
  19.   status = napi_create_async_work(env, nullptr, resource_name, work, done, (void *) ptr, &ptr->worker); 
  20.   if (status != napi_ok) { 
  21.     goto done; 
  22.   } 
  23.   // 提及work到Libuv 
  24.   status = napi_queue_async_work(env, ptr->worker); 
  25.  
  26.   done:  
  27.     napi_value ret; 
  28.     napi_create_int32(env, status == napi_ok ? 0 : -1, &ret); 
  29.     return  ret; 

执行上面的函数,任务就会被提交到Libuv线程池了。

3 Libuv子线程执行任务

  1. void work(napi_env env, void* data) { 
  2.   struct info *arg = (struct info *)data; 
  3.   printf("doing...\n"); 
  4.   int sum = 0; 
  5.   for (int i = 0; i < 10; i++) { 
  6.     sum += i; 
  7.   } 
  8.   arg->sum = sum

很简单,计算几个数。并且保存结果。

4 回调js

  1. void done(napi_env env, napi_status status, void* data) { 
  2.   struct info *arg = (struct info *)data; 
  3.   if (status == napi_cancelled) { 
  4.     printf("cancel..."); 
  5.   } else if (status == napi_ok) { 
  6.     printf("done...\n"); 
  7.     napi_value callback; 
  8.     napi_value global;   
  9.     napi_value result; 
  10.     napi_value sum
  11.     // 拿到结果 
  12.     napi_create_int32(env, arg->sum, &sum); 
  13.     napi_get_reference_value(env, arg->func, &callback); 
  14.     napi_get_global(env, &global); 
  15.     // 回调js 
  16.     napi_call_function(env, global, callback, 1, &sum, &result); 
  17.     // 清理 
  18.     napi_delete_reference(env, arg->func); 
  19.     napi_delete_async_work(env, arg->worker); 
  20.   } 

并且执行后,我们看到输出了45。接下来我们分析大致的过程。首先我呢看看ThreadPoolWork,ThreadPoolWork是对Libuv work的封装。

  1. class ThreadPoolWork { 
  2.  public
  3.   explicit inline ThreadPoolWork(Environment* env) : env_(env) { 
  4.     CHECK_NOT_NULL(env); 
  5.   } 
  6.   inline virtual ~ThreadPoolWork() = default
  7.  
  8.   inline void ScheduleWork(); 
  9.   inline int CancelWork(); 
  10.  
  11.   virtual void DoThreadPoolWork() = 0; 
  12.   virtual void AfterThreadPoolWork(int status) = 0; 
  13.  
  14.   Environment* env() const { return env_; } 
  15.  
  16.  private: 
  17.   Environment* env_; 
  18.   uv_work_t work_req_; 
  19. }; 

类的定义很简单,主要是封装了uv_work_t。我们看看每个函数的意义。DoThreadPoolWork和AfterThreadPoolWork是虚函数,由子类实现,我们一会看子类的时候再分析。我们看看ScheduleWork

  1. void ThreadPoolWork::ScheduleWork() { 
  2.   env_->IncreaseWaitingRequestCounter(); 
  3.   int status = uv_queue_work( 
  4.       env_->event_loop(), 
  5.       &work_req_, 
  6.       // Libuv子线程里执行的任务函数 
  7.       [](uv_work_t* req) { 
  8.         ThreadPoolWork* self = ContainerOf(&ThreadPoolWork::work_req_, req); 
  9.         self->DoThreadPoolWork(); 
  10.       }, 
  11.       // 任务处理完后的回调 
  12.       [](uv_work_t* req, int status) { 
  13.         ThreadPoolWork* self = ContainerOf(&ThreadPoolWork::work_req_, req); 
  14.         self->env_->DecreaseWaitingRequestCounter(); 
  15.         self->AfterThreadPoolWork(status); 
  16.       }); 
  17.   CHECK_EQ(status, 0); 

ScheduleWork是负责给Libuv提交任务的函数。接着看看CancelWork。

  1. int ThreadPoolWork::CancelWork() { 
  2.   return uv_cancel(reinterpret_cast<uv_req_t*>(&work_req_)); 

直接调用Libuv的函数取消任务。看完父类,我们看看子类的定义,子类在N-API里实现。

  1. class Work : public node::AsyncResource, public node::ThreadPoolWork { 
  2.  private: 
  3.   explicit Work(node_napi_env env, 
  4.                 v8::Local<v8::Object> async_resource, 
  5.                 v8::Local<v8::String> async_resource_name, 
  6.                 napi_async_execute_callback execute
  7.                 napi_async_complete_callback complete = nullptr, 
  8.                 void* data = nullptr) 
  9.     : AsyncResource(env->isolate, 
  10.                     async_resource, 
  11.                     *v8::String::Utf8Value(env->isolate, async_resource_name)), 
  12.       ThreadPoolWork(env->node_env()), 
  13.       _env(env), 
  14.       _data(data), 
  15.       _execute(execute), 
  16.       _complete(complete) { 
  17.   } 
  18.  
  19.   ~Work() override = default
  20.  
  21.  public
  22.   static Work* New(node_napi_env env, 
  23.                    v8::Local<v8::Object> async_resource, 
  24.                    v8::Local<v8::String> async_resource_name, 
  25.                    napi_async_execute_callback execute
  26.                    napi_async_complete_callback complete, 
  27.                    void* data) { 
  28.     return new Work(env, async_resource, async_resource_name, 
  29.                     execute, complete, data); 
  30.   } 
  31.   // 释放该类对象的内存 
  32.   static void Delete(Workwork) { 
  33.     delete work
  34.   } 
  35.   // 执行用户设置的函数 
  36.   void DoThreadPoolWork() override { 
  37.     _execute(_env, _data); 
  38.   } 
  39.  
  40.   void AfterThreadPoolWork(int status) override { 
  41.    // 执行用户设置的回调 
  42.     _complete(env, ConvertUVErrorCode(status), _data); 
  43.   } 
  44.  
  45.  private: 
  46.   node_napi_env _env; 
  47.   // 用户设置的数据,用于保存执行结果等 
  48.   void* _data; 
  49.   // 执行任务的函数 
  50.   napi_async_execute_callback _execute; 
  51.   // 任务处理完的回调 
  52.   napi_async_complete_callback _complete; 
  53. }; 

在Work类我们看到了虚函数DoThreadPoolWork和AfterThreadPoolWork的实现,没有太多逻辑。最后我们看看N-API提供的API的实现。

  1. napi_status napi_create_async_work(napi_env env, 
  2.                                    napi_value async_resource, 
  3.                                    napi_value async_resource_name, 
  4.                                    napi_async_execute_callback execute
  5.                                    napi_async_complete_callback complete, 
  6.                                    void* data, 
  7.                                    napi_async_work* result) { 
  8.   v8::Local<v8::Context> context = env->context(); 
  9.  
  10.   v8::Local<v8::Object> resource; 
  11.   if (async_resource != nullptr) { 
  12.     CHECK_TO_OBJECT(env, context, resource, async_resource); 
  13.   } else { 
  14.     resource = v8::Object::New(env->isolate); 
  15.   } 
  16.  
  17.   v8::Local<v8::String> resource_name; 
  18.   CHECK_TO_STRING(env, context, resource_name, async_resource_name); 
  19.  
  20.   uvimpl::Workwork = uvimpl::Work::New(reinterpret_cast<node_napi_env>(env), 
  21.                                          resource, 
  22.                                          resource_name, 
  23.                                          execute
  24.                                          complete, 
  25.                                          data); 
  26.  
  27.   *result = reinterpret_cast<napi_async_work>(work); 
  28.  
  29.   return napi_clear_last_error(env); 

napi_create_async_work本质上是对Work的简单封装,创建一个Work并返回给用户。

2 napi_delete_async_work

  1. napi_status napi_delete_async_work(napi_env env, napi_async_work work) { 
  2.   CHECK_ENV(env); 
  3.   CHECK_ARG(env, work); 
  4.  
  5.   uvimpl::Work::Delete(reinterpret_cast<uvimpl::Work*>(work)); 
  6.  
  7.   return napi_clear_last_error(env); 

napi_delete_async_work用于任务执行完后释放Work对应的内存。

3 napi_queue_async_work

  1. napi_status napi_queue_async_work(napi_env env, napi_async_work work) { 
  2.   CHECK_ENV(env); 
  3.   CHECK_ARG(env, work); 
  4.  
  5.   napi_status status; 
  6.   uv_loop_t* event_loop = nullptr; 
  7.   status = napi_get_uv_event_loop(env, &event_loop); 
  8.   if (status != napi_ok) 
  9.     return napi_set_last_error(env, status); 
  10.  
  11.   uvimpl::Work* w = reinterpret_cast<uvimpl::Work*>(work); 
  12.  
  13.   w->ScheduleWork(); 
  14.  
  15.   return napi_clear_last_error(env); 

napi_queue_async_work是对ScheduleWork的封装,作用是给Libuv线程池提交任务。

4 napi_cancel_async_work

  1. napi_status napi_cancel_async_work(napi_env env, napi_async_work work) { 
  2.   CHECK_ENV(env); 
  3.   CHECK_ARG(env, work); 
  4.  
  5.   uvimpl::Work* w = reinterpret_cast<uvimpl::Work*>(work); 
  6.  
  7.   CALL_UV(env, w->CancelWork()); 
  8.  
  9.   return napi_clear_last_error(env); 

napi_cancel_async_work是对CancelWork的封装,即取消Libuv线程池的任务。我们看到一层层套,没有太多逻辑,主要是要符合N-API的规范。

总结:通过N-API提供的API,使得我们不再受限于Nod.js本身提供的一些异步接口(使用Libuv线程池的接口),而是直接使用Libuv线程池,这样我们不仅可以自己写c/c++,还可以复用业界的一些解决方案解决Node.js里的一些耗时任务。

仓库:https://github.com/theanarkh/learn-to-write-nodejs-addons

 

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2021-06-06 08:30:29

N-APIPromiseAPI

2020-09-07 07:33:01

NodejsCPU密集型

2012-02-29 13:26:20

Java

2023-08-02 08:03:08

Python线程池

2021-09-11 15:26:23

Java多线程线程池

2021-02-06 14:02:55

线程池Builder模式

2021-07-11 23:25:29

Libuvepoll文件

2023-05-19 08:01:24

Key消费场景

2023-06-08 07:48:03

Java线程池

2021-02-01 08:28:24

Linux线程池Linux系统

2020-04-29 14:10:44

Java线程池编程语言

2023-10-12 08:29:06

线程池Java

2021-06-06 23:40:53

线程池使用场景

2014-12-24 10:00:07

Spring

2023-06-07 13:49:00

多线程编程C#

2015-03-24 16:29:55

默认线程池java

2022-03-05 23:29:18

LibuvwatchdogNode.js

2020-12-10 08:24:40

线程池线程方法

2012-05-15 02:18:31

Java线程池

2023-10-13 08:20:02

Spring线程池id
点赞
收藏

51CTO技术栈公众号