OceaBase开发者大会落地上海!4月20日共同探索数据库前沿趋势!报名戳 了解详情
写点什么

Node.js 异步处理 CPU 密集型任务的新思路

  • 2014-06-17
  • 本文字数:3828 字

    阅读完需:约 13 分钟

Node.js 擅长数据密集型实时(data-intensive real-time)交互的应用场景。然而数据密集型实时应用程序并不是只有 I/O 密集型任务,当碰到 CPU 密集型任务时,比如要对数据加解密(node.bcrypt.js),数据压缩和解压(node-tar),或者要根据用户的身份对图片做些个性化处理,在这些场景下,主线程致力于做复杂的 CPU 计算,I/O 请求队列中的任务就被阻塞。

Node.js 主线程的 event loop 在处理所有的任务 / 事件时,都是沿着事件队列顺序执行的,所以在其中任何一个任务 / 事件本身没有完成之前,其它的回调、监听器、超时、nextTick() 的函数都得不到运行的机会,因为被阻塞的 event loop 根本没机会处理它们,此时程序最好的情况是变慢,最糟的情况是停滞不动,像死掉一样。

一个可行的解决方案是新开进程,通过 IPC 通信,将 CPU 密集型任务交给子进程,子进程计算完毕后,再通过 ipc 消息通知主进程,并将结果返回给主进程。

和创建线程相比,开辟新进程的系统资源占用率大,进程间通信效率也不高。如果能不开新进程而是新开线程,将 CPU 耗时任务交给一个工作线程去做,然后主线程立即返回,处理其他的 I/O 请求,等到工作线程计算完毕后,通知主线程并将结果返回给主线程。那么在同时面对 I/O 密集型和 CPU 密集型服务的场景下,Node 的主线程也会变得轻松,并能时刻保持高响应度。

因此,和开进程相比,一个更加优秀的解决方案是:

  1. 不开进程,而是将 CPU 耗时操作交给进程内的一个工作线程完成。
  2. CPU 耗时操作的具体逻辑支持通过 C++ 和 JS 实现。
  3. JS 使用这个机制与使用 I/O 库类似,方便高效。
  4. 在新线程中运行一个独立的 V8 VM,与主线程的 VM 并发执行,并且这个线程必须由我们自己托管。

为了实现以上四个目标,我们在 Node 中增加了一个 backgroundthread 线程,文章稍候会详细解释这个概念。在具体实现上,为 Node 增加了一个 pt_c 的内建 C++ 模块。这个模块负责把 CPU 耗时操作封装成一个 Task,抛给 backgroundthread,然后立即返回。具体的逻辑在另一个线程中处理,完成之后,设定结果,通知主线程。这个过程非常类似于异步 I/O 请求。具体逻辑如下图:

Node 提供了一种机制可以将 CPU 耗时操作交给其他线程去做,等到执行完毕后设置结果通知主线程执行 callback 函数。以下是一段代码,用来演示这个过程:

复制代码
int main() {
loop = uv_default_loop();
int data[FIB_UNTIL];
uv_work_t req[FIB_UNTIL];
int i;
for (i = 0; i < FIB_UNTIL; i++) {
data[i] = i;
req[i].data = (void *) &data[i];
uv_queue_work(loop, &req[i], fib, after_fib);
}
return uv_run(loop, UV_RUN_DEFAULT);
}

其中函数 uv_queue_work 的定义如下:

复制代码
UV_EXTERN int uv_queue_work(uv_loop_t* loop,
uv_work_t* req,
uv_work_cb work_cb,
uv_after_work_cb after_work_cb);

参数 work_cb 是在另外线程执行的函数指针,after_work_cb 相当于给主线程执行的回调函数。 在 windows 平台上,uv_queue_work 最终调用 API 函数 QueueUserWorkItem 来派发这个 task,最终执行 task 的线程是由操作系统托管的,每次可能都不一样。这不满足上述第四条。

因为我们要支持在线程中运行 js 代码,这就需要开一个 V8 VM,所以需要把这个线程固定下来,特定任务,只交给这个线程处理。并且一旦创建,不管有没有 task,都不能随便退出。这就需要我们自己维护一个线程对象,并且提供接口,使得使用者可以方便的生成一个对象并且提交给这个线程的任务队列。

在绑定内建模块 pt_c 的时候,会创建一个 background thread 的线程对象。这个线程拥有一个 taskloop,有任务就处理,没有任务就等待在一个信号量上。多线程要考虑线程间同步的问题。线程同步只发生在读写此线程的 incomming queue 的时候。Node 的主线程生成 task 后,提交到这个线程的 incomming queue 中,并激活信号量然后立即返回。在下一次循环中,backgroundthread 从 incomming queue 中取出所有的 task,放入 working queue,然后依次执行 working queue 中的 task。主线程不访问 working queue 因此不需要加锁。这样做可以降低冲突。

这个线程在进入 taskloop 循环之前会建立一个独立的 V8 VM,专门用来执行 backgroundjs 的代码。主线程的 v8 引擎和这个线程的可以并行执行。它的生命周期与 Node 进程的生命周期一致。

复制代码
// pt_c 模块的初始化代码
void Init(Handle<Object> target,
Handle<Value> unused,
Handle<Context> context,
void* priv) {
//Create working thread, focus on cup intensive task
if(!CWorkingThread::GetInstance().Start()){
return;
}
Environment* env = Environment::GetCurrent(context);
// load dll, Including all the cpu-intensive functions
NODE_SET_METHOD(target, "registermodule", RegisterModule);
NODE_SET_METHOD(target, "posttask", PostTask);
// post a task that run a cpu-intensive function defined in backgroundjs
NODE_SET_METHOD(target, "jstask", JsTask);
}

可以把所有 CPU 耗时逻辑放入 backgroundJs 中,主线程通过生成一个 task,指定好运行的函数和参数,抛给工作线程。工作线程在执行 task 的过程中调用在 backgroundJs 中的函数。BackgroundJs 是一个.js 文件,在里面添加 CPU 耗时函数。

background.js 代码示例:

复制代码
var globalFunction = function(v){
var obj;
try {
obj = JSON.parse(v);
 } catch(e) {
return e;
}
 var a = obj.param1;
 var b = obj.param2;
 var i;
 // simulate CPU intensive process...
 for(i = 0; i < 95550000; ++i) {
   i += 100;
i -= 100;
 }
return (a + b).toString();
}

运行 Node,在控制台输入:

复制代码
var bind = process.binding('pt_c');
var obj = {param1: 123,param2: 456};
bind.jstask('globalFunction', JSON.stringify(obj), function (err, data) {
if (err) {
console.log("err");
} else {
console.log(data);
}
});

调用的方法是 bind.jstask,稍后会解释这个函数的用法。

以下是测试结果:

上面这个实验操作步骤如下:

  1. 首先绑定 pt_c 内建模块。绑定的过程会调用模块初始化函数,在这个函数中,创建新线程。
  2. 快速多次调用 backgroundjs 中的 CPU 耗时函数,上面的实验中连续调用了三次。

当 backgroundjs 中的函数完成后,主线程接到通知, 在新一轮的 evenloop 中,调用回调函数,打印出结果。这个实验说明了 CPU 耗时操作异步执行。

方法 jstask 总共三个参数,前两个参数为字符串,分别是 background.js 中的全局函数名称,传给函数的参数。最后一个参数是一个 callback 函数,异步留给主线程运行。

为什么用字符串做参数?

为了适应各种不同的参数类型,就需要为 C++ 函数提供各种不同的函数实现,这是非常受限制的。C++ 根据函数名获取 backgroundjs 中的函数然后将参数传递给 js。在 js 中,处理 json 字符串是非常容易的,因此采用字符串,简化了 C++ 的逻辑,js 又能够方便的生成和解析参数。同样的理由,backgroundjs 中函数的返回值也为 json 串。

对 C++ 的支持

在苛求性能的场景,pt_c 允许加载一个.dll 文件到 node 进程,这个 dll 文件包含 CPU 耗时操作。js 加载 pt_c 的时候,指定文件名即可完成加载。

代码示例:

复制代码
var bind = process.binding('pt_c');
bind.registermodule('node_pt_c.dll', 'DllInit', 'Json to Init');
bind.posttask('Func_example', 'Json_Param', function (err, data) {
if (err) {
console.log("err");
} else {
console.log(data);
}
});

与 backgroundjs 相比,加载 C++ 模块多了一个步骤,这个步骤是调用 bind.registermodule。这个函数负责将加载 dll 并负责对其初始化。一旦成功后,不能再加载其他模块。所有的 CPU 耗时操作函数都应该在这个 dll 文件中实现。

总结

这篇文章提出了 backgroundjs 这个新的概念,扩展了 Node.js 的能力, 解决了 Node 在处理 CPU 密集任务时的短板。这个解决方案使得使用 Node 的开发人员只需要关注 backgroundjs 中的函数。比起多开进程或者新添加模块的解决方案更高效,通用和一致。我们的代码已经开源,您可以在 https://github.com/classfellow/node/tree/Ansy-CPU-intensive-work--in-one-process 下载。

支持 backgroundjs 一个稳定 Node 版本您可以在 http://www.witch91.com/nodejs.rar 下载。

参考文献

  1. Node.js 软肋之 CPU 密集型任务
  2. Why you should use Node.js for CPU-bound tasks,Neil Kandalgaonkar,2013.4.30;
  3. http://nikhilm.github.io/uvbook/threads.html#inter-thread-communication
  4. 深入浅出 Node.js 朴灵

补充和校正

上文叙述了如何扩展 node,建立一种事件驱动的解决 CPU 密集型任务的机制。但有几个局限:

1 代码实现上只做了 Windows,没有对 Linux 的支持;
2 源代码级扩展 node,需要下载分支代码编译 node;
3 缺少对 C++ 层面的扩展支持。

后来用 node 扩展模块的方式重新实现为 rcib 模块,改进如下:

1 增加了对 Linux 的支持;
2 通过 npm install rcib 安装,即作为一个 node C++ 第三方扩展使用;
3 增加了 C++ 扩展的支持,rcib 本身可作为一种扩展模式,可以方便的在此基础上,实现基于事件驱动的 C++ 扩展,任意扩展 node 能力;
4 源码位置 : https://github.com/classfellow/rcib

感谢田永强对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2014-06-17 00:2013668

评论

发布
暂无评论
发现更多内容

集世界杯+GameFi元素的MetaElfLand,为何将在世界杯期间爆发?

鳄鱼视界

集世界杯+GameFi元素的MetaElfLand,推出世界杯专场活动

西柚子

华为云安全亮相世界互联网大会

科技怪授

华为云

使用 Bytebase 管理 Rainbond 上的应用数据库

北京好雨科技有限公司

Nginx负载均衡配置、限流配置、Https配置详解

C++后台开发

nginx 负载均衡 HTTP 后端开发 C++开发

全面焕新|详解 Grafana v9.0.x 新增功能特性

阿里巴巴云原生

阿里云 云原生 Grafana 新功能

全球6位IT负责人解读数字化转型不断失败的原因

雨果

数字化转型

集世界杯+GameFi元素的MetaElfLand,推出世界杯专场活动

EOSdreamer111

一文详解GaussDB(DWS) 的并发管控和内存管控

华为云开发者联盟

大数据 后端 华为云

温州有等保测评机构吗?听说没有是吗?

行云管家

等保 等保测评

Baklib帮助中心:自助服务指南

Baklib

客户服务 帮助中心

不懂“数据服务”,聊什么“数据中台”

雨果

数据中台 数据服务

如何在 Rocky Linux 上安装 Apache Kafka?

wljslmz

Apache kafka 11月月更

【论文复现】Panoptic Deeplab(全景分割PyTorch)

华为云开发者联盟

人工智能 华为云 论文复现

大厂数据开发老司机送给数据工程师的10条建议,建议先收藏再细品!

雨果

数据开发

Java岗位必备技能SpringBoot的9道面试题集锦

钟奕礼

Java java程序员 java面试 java编程

二面被RocketMQ虐后,狂刷这套实战到源码手册,再战阿里

钟奕礼

Java Java 面试 java程序员 java编程

Zebec开启多链布局,流支付生态持续扩张

西柚子

2023年值得学习的云计算技术有哪些?

wljslmz

云计算 11月月更

华为云GaussDB打造金融行业坚实数据底座,共创数字金融新未来

清欢科技

5G+云渲染,助力虚拟仿真医学实训

Finovy Cloud

云渲染

集世界杯+GameFi元素的MetaElfLand,为何将在世界杯期间爆发?

股市老人

OneFlow-ONNX v0.6.0正式发布

OneFlow

人工智能 深度学习

MemArts :高效解决存算分离架构中数据访问的组件

华为云开发者联盟

云计算 后端 华为云

大规模 Spring Cloud 微服务无损上下线探索与实践

阿里巴巴云原生

阿里云 微服务 云原生 Spring Cloud

kafka实战】分区重分配可能出现的问题和排查问题思路

石臻臻的杂货铺

kafka Kafka实战 11月月更

刘强东给京东高管降薪:2千多位总监兄弟工资打8折,并拿出100亿保障”兄弟“基础住房

小小怪下士

程序员 京东 刘强东

offset新探索:双管齐下,加速大数据量查询

IT科技苏辞

NFT盲盒链游DAPP系统开发搭建技术

薇電13242772558

web3

华为再次入选2022年Gartner® SIEM魔力象限

科技怪授

华为云

万恶的strpos函数

J.Smile

自学php

Node.js异步处理CPU密集型任务的新思路_架构/框架_尤嘉_InfoQ精选文章