原文来自于:http://www.infoq.com/cn/articles/new-idea-of-nodejs-asynchronous-processing-tasks?utm_source=infoq&utm_medium=popular_links_homepage

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.js异步处理CPU密集型任务的新思路的更多相关文章

  1. Node.js异步处理CPU密集型任务

    Node.js异步处理CPU密集型任务 Node.js擅长数据密集型实时(data-intensive real-time)交互的应用场景.然而数据密集型实时应用程序并非仅仅有I/O密集型任务,当碰到 ...

  2. 深入理解node.js异步编程:基础篇

    ###[本文是基础内容,大神请绕道,才疏学浅,难免纰漏,请各位轻喷] ##1. 概述 目前开源社区最火热的技术当属Node.js莫属了,作为使用Javascript为主要开发语言的服务器端编程技术和平 ...

  3. node.js异步编程的几种模式

    Node.js异步编程的几种模式 以读取文件为例: 1.callback function const fs = require('fs'); //callback function fs.readF ...

  4. node.js异步编程解决方案之Promise用法

    node.js异步编程解决方案之Promise var dbBase = require('../db/db_base'); var school_info_db = require('../db/s ...

  5. Node.js 异步模式浅析

    注:此文是node.js实战读后的总结. 在平常的脚本语言中都是同步进行的,比如php,服务器处理多个请求的方法就是并行这些脚本.多任务处理,多线程等等.但是这种处理方式也有一个问题:每一个进程或者线 ...

  6. node.js 异步式I/O 与事件驱动

    Node.js 最大的特点就是异步式 I/O(或者非阻塞 I/O)与事件紧密结合的编程模式.这种模式与传统的同步式 I/O 线性的编程思路有很大的不同,因为控制流很大程度上要靠事件和回调函数来组织,一 ...

  7. Node.js 异步异闻录

    本文首发在个人博客:http://muyunyun.cn/posts/7b9fdc87/ 提到 Node.js, 我们脑海就会浮现异步.非阻塞.单线程等关键词,进一步我们还会想到 buffer.模块机 ...

  8. Node.js异步IO原理剖析

    为什么要异步I/O? 从用户体验角度讲,异步IO可以消除UI阻塞,快速响应资源 JavaScript是单线程的,它与UI渲染共用一个线程.所以在JavaScript执行的时候,UI渲染将处于停顿的状态 ...

  9. node js 异步运行流程控制模块Async介绍

    1.Async介绍 sync是一个流程控制工具包.提供了直接而强大的异步功能.基于Javascript为Node.js设计,同一时候也能够直接在浏览器中使用. Async提供了大约20个函数,包含经常 ...

随机推荐

  1. myeclipse 闪退解决方法

    今天提交一个svn文件发生卡死,然后关闭myeclipse后发生,打开myeclipse出险闪退,摸索半天,发现: 只要修改下工作空间的名称,然后打开myeclipse重新导入即可,只是之前的配置都没 ...

  2. 《JS原型》

    @(JavaScript原型) JavaScript中最晦涩难懂的恐怕就是原型了.故以此笔记本来记录原型的学习过程,日后忘了可来温习. 一切皆为对象 null--Object&Function ...

  3. Android Developers:两个视图渐变

    淡入淡出动画(也被称为渐隐)逐渐淡出一个UI组件,同时淡入另一个.这个动画在你想在你的应用程序中切换内容或者是视图的情况下非常有用.淡入淡出非常微妙并短,但支持从一个屏幕到下一个屏幕流畅的过渡.当你不 ...

  4. Java读书笔记二(封装类)

    1.介绍 都知道java中基本数据类型有非常多,比方string,int--,可是基本数据类型与对象之间是不同的.但非常多情况下,我们希望将基本数据类型当作对象使用,这时候就须要用到封装类. 2.封装 ...

  5. Ubuntu server下安装JDK和Tomcat7

    服务器是Ubuntu server 12.04 LTS 64bit 所有操作假设已经有root权限,若没有需要添加sudo. 一. 安装JDK 1.去Oracle官网下载jdk-6u45-linux- ...

  6. Linux 监控CPU 温度

      安装测试系统: 硬件:普通PC机, 软件:redhat linux as 4  2.6 .9 , 安装系统自带的lm_sensors-2.8.7-2.i386 你也可以从[url]http://w ...

  7. (转)cocos2dx 内存管理

    原文地址:http://blog.csdn.net/ring0hx/article/details/7946397 cocos2dx的内存管理移植自Objective-C, 对于没有接触过OC的C++ ...

  8. [转] 条件变量(Condition Variable)详解

    http://www.wuzesheng.com/?p=1668 条件变量(Condtion Variable)是在多线程程序中用来实现“等待->唤醒”逻辑常用的方法.举个简单的例子,应用程序A ...

  9. 体验Impress.js

    用腻了ppt,prezi的风格看起来更酷一点儿,无意中得知有Impress.js这么一个H5版的prezi,nice,值得一试. 关于Impress.js,网上教程很多,但说实话就跟教小朋友一样,一步 ...

  10. Block小结

    Blocks是C语言的扩充功能.用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数. block其实是一个代码块,但是它的神奇之处在于在内联(inline)执行的时候(这和C++ ...