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字符串是非常容易的,因此采用字符串,简化了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

Node.js异步处理CPU密集型任务,布布扣,bubuko.com

时间: 2024-12-19 02:17:23

Node.js异步处理CPU密集型任务的相关文章

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

原文来自于: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

node js异步IO机制

同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行:而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数. 阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间:而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成. node js的异步I/O是它的一个重要功能,为了讲清楚这个机制,先说

深入理解node.js异步编程

1. 概述目前开源社区最火热的技术当属Node.js莫属了,作为使用Javascript为主要开发语言的服务器端编程技术和平台,一开始就注定会引人瞩目. 当然能够吸引众人的目光,肯定不是三教九流之辈,必然拥有独特的优势和魅力,才能引起群猿追逐.其中当属异步IO和事件编程模型,本文据Node.js的异步IO和事件编程做深入分析. ##2. 什么是异步同步和异步是一个比较早的概念,大抵在操作系统发明时应该就出现了.举一个最简单的生活中的例子,比如发短信的情况会比较好说明他们的区别:同步:正在处于苦逼

node js 异步执行流程控制模块Async介绍

1.Async介绍 sync是一个流程控制工具包,提供了直接而强大的异步功能.基于Javascript为Node.js设计,同时也可以直接在浏览器中使用. Async提供了大约20个函数,包括常用的 map, reduce, filter, forEach 等,异步流程控制模式包括,串行(series),并行(parallel),瀑布(waterfall)等. 项目地址:https://github.com/caolan/async 2. Async安装 npm install async 3.

避免多层回调,Node.js异步模块Async初使用

原来写的一个分页查询,回调了好几层. exports.list = function(req,res) { var params = {}; var current_page = common_util.get_param_value(req,'current_page','Number',1); var page_size = common_util.get_param_value(req,'page_size','Number',10); var start_index = common_u

避免多层回调,Node.js异步库Async使用(series)

未使用Async之前coffeescript写的代码: exports.product_file_add = (req,res) -> if !req.param('file_id') return res.json({'flag':'error','msg':'请先上传文件再保存!'}) file_type = req.param('file_type') #判断产品和文件类型,限制上传的数量 params = {} params.product_code = req.param('produ

Node.js异步流程控制

原文地址:Node.js异步流程控制 原文地址:https://www.cnblogs.com/edward852/p/8580917.html

Node.js异步IO

为什么要异步I/O? 从用户体验角度讲,异步IO可以消除UI阻塞,快速响应资源 JavaScript是单线程的,它与UI渲染共用一个线程.所以在JavaScript执行的时候,UI渲染将处于停顿的状态,用户体验较差.而异步请求可以在下载资源的时候,JavaScript和UI渲染都同时执行,消除UI阻塞,降低响应资源需要的时间开销. 假如一个资源来自两个不同位置的数据的返回,第一个资源需要M毫秒的耗时,第二个资源需要N毫秒的耗时.当采用同步的方式,总耗时为(M+N)毫秒,代码大致如下: //耗时为

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

Node.js 最大的特点就是异步式 I/O(或者非阻塞 I/O)与事件紧密结合的编程模式.这种模式与传统的同步式 I/O 线性的编程思路有很大的不同,因为控制流很大程度上要靠事件和回调函数来组织,一个逻辑要拆分为若干个单元. 阻塞与线程什么是阻塞(block)呢?线程在执行中如果遇到磁盘读写或网络通信(统称为 I/O 操作),通常要耗费较长的时间,这时操作系统会剥夺这个线程的 CPU 控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为 阻塞.当 I/O 操作完毕时,操作系