原文来自于: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. JAX-WS + Spring 开发webservice

    通过几天的时间研究了下使用jax-ws来开发webservice,看了网上的一些资料总结出jax-ws的开发大概分为两种. 以下项目使用的spring3.0,jar包可以到官网下载 第一种:使用独立的 ...

  2. stl_alloc.h

    /* * Copyright (c) 1996-1997 * Silicon Graphics Computer Systems, Inc. * * Permission to use, copy, ...

  3. 500 OOPS: vsftpd: refusing to run with writable root inside chroot()解决方法

    vsftpd.conf配置文件如下: [root@rusky ~]# cat /etc/vsftpd/vsftpd.conf | grep -v "#" anonymous_ena ...

  4. Upgrade to Python 2.7.9 on CentOS5.5

    1. Install python2.7 #cd /tmp #wget https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz --no-ch ...

  5. NYOJ2括号配对问题

    括号配对是最基本的栈的问题,它是栈入门的经典题目,思路是,如果是左括号直接进栈,如果是右括号,这时就要比较栈顶的元素与他是否匹配,如果匹配则出栈,否则进栈,下面是代码的实现: #include < ...

  6. 176. [USACO Feb07] 奶牛聚会

    #include<iostream> #include<cstdio> #include<cstring> #include<queue> #defin ...

  7. FineUI属性的简单总结

    .PageManager控件— 页面级别的控制(包括主题.语言 等等) 覆盖web.config中自定义结点的配置 EnablePageLoading:是否启用页面的第一次加载提示,默认居中显示加载图 ...

  8. (转)php 函数名称前的@有什么作用

    如:$register_globals = @ini_get('register_globals'); 隐藏错误提示~如果ini_get('register_globals'); 语句错误的话`错误会 ...

  9. Android开发手记(24) Log的使用及颜色的更改

    在程序开发过程中,LOG是广泛使用的用来记录程序执行过程的机制,它既可以用于程序调试,也可以用于产品运营中的事件记录.在Android系统中,提供了简单.便利的LOG机制,开发人员可以方便地使用.本文 ...

  10. Message,MessageQueue,Looper,Handler ——由view.post(runnable想到的)

    近日看到代码有view.post(runable),发现对handler机制又有些模糊,故做些复习. 这里就不再对具体的源码原理做深入复习了,就抄一些基本的结论吧. 1.基本概念 Message:基本 ...