Node.js异步处理CPU密集型任务
Node.js擅长数据密集型实时(data-intensive real-time)交互的应用场景。然而数据密集型实时应用程序并非仅仅有I/O密集型任务,当碰到CPU密集型任务时,比方要对数据加解密(node.bcrypt.js),数据压缩和解压(node-tar),或者要依据用户的身份对图片做些个性化处理,在这些场景下,主线程致力于做复杂的CPU计算,IO请求队列中的任务就被堵塞。
Node.js主线程的event loop在处理全部的任务/事件时,都是沿着事件队列顺序执行的,所以在当中不论什么一个任务/事件本身没有完毕之前,其他的回调、监听器、超时、nextTick()的函数都得不到执行的机会,由于被堵塞的event loop根本没机会处理它们,此时程序最好的情况是变慢,最糟的情况是停滞不动,像死掉一样。
一个可行的解决方式是新开进程,通过ipc通信,将CPU密集型任务交给子进程,子进程计算完成后,再通过ipc消息通知主进程,并将结果返回给主进程[1]。
和创建线程相比,开辟新进程的系统资源占用率大,进程间通信效率也不高。假设能不开新进程而是新开线程,将CPU耗时任务交给一个工作线程去做,然后主线程马上返回,处理其它的IO请求,等到工作线程计算完成后,通知主线程并将结果返回给主线程。那么在同一时候面对IO密集型和CPU密集型服务的场景下,Node.js的主线程也会变得轻松,并能时刻保持高对应度。
因此,和开进程相比,一个更加优秀的解决方式是:
1 不开进程,而是将CPU耗时操作交给进程内的一个工作线程完毕。
2 CPU耗时操作的详细逻辑支持通过C++和Js实现。
3 js使用这个机制与使用IO库类似,方便高效。
4 在新线程中执行一个独立的V8 VM,与主线程的VM并发执行,而且这个线程必须 由我们自己托管。
为了实现以上四个目标,我们在Node中添加�了一个backgroundthread线程,文章稍候会详解这个概念。在详细实现上,为Node添加�了一个’pt_c’的内建C++模块。这个模块负责吧CPU耗时操作封装成一个Task,抛给backgroundthread,然后马上返回。详细的逻辑在还有一个线程中处理,完毕之后,设定结果,通知主线程。这个过程很类似于异步IO请求。详细逻辑例如以下图:
BackgroundThread
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,都不能随便退出。这就须要我们自己维护一个线程对象,而且提供接口,使得使用者能够方便的生成一个对象而且提交给这个线程的任务队列。
在node进程启动初始化过程中,添�一个创建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进程的生命周期一致。
BackgroundJs
能够把全部CPU耗时逻辑放入backgroundJs中,主线程通过生成一个task,指定好执行的函数和參数,抛给工作线程。工作线程在执行task的过程中调用在backgroundJs中的函数。BackgroundJs是一个.js文件,在里面加入�CPU耗时函数。
background.js代码演示样例:
var globalFunction = function(v){
var flag;
try
{
flag = true;
JSON.parse(v);
}
catch(e)
{
flag = false;
}
if(!flag)
{
var err = 'err';
return err;
}
var obj = JSON.parse(v);
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.js,在控制台输入:
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字符串是很easy的,因此採用字符串,简化了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密集型任务
http://www.infoq.com/cn/articles/nodejs-weakness-cpu-intensive-tasks/
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