十一、Node.js异步流程控制(序列模式、并发模式、有限并发模式)
Javascript在基本语法上与其它大部份C派生的语言没有太多区别,你可能很容易从其它语言过度到Javascript。很多从其它语言转到Javascript来的用户,在用一段时间之后很可能对这门语言又爱又恨,特别是对于异步流程的控制。
对于大部份异步编程的模型来说,大多是事件驱动型且是基于进程来编码。这样为我们带来了诸多好处,我们不必去处理为了实现同样目的而做的多线程模型里面的问题。多线程编程里面,由于多个线程访问的内存块是一样的,所以可能会出现死锁、共享竞争等问题。
异步编程是什么昵?打个比方,你需要准备一个丰富的晚餐,其中有炒菜与熬汤。如果是阻塞式同步编程的话,你需要先去炒菜,等菜炒好了,然后在去熬汤。在炒菜的过程中你不能在做其它事情,只有两样都好了的时候你才能开始你的晚餐。那异步会怎么做昵?可以一边熬汤,一边炒菜。菜炒好的时候,可能汤也好了。然后你就可以开动了。如果以每一个步骤的时间来计算的话,两者的时间成本里面显然异步要能做的事情更多。
阻塞式同步编程:炒菜(5分钟)+熬汤(30分钟)=开饭时用了35分钟
异步式编程模型:【炒菜(5分钟)熬汤(30分钟)】=开饭时用了30分钟
通过以上的例子可以知道异步编程模型在完全任务的时间比以及时间利用上要高效的多。
研究异步流处理的意义
异步流程控制不好,很容易写出巢式风格代码,随着项目的深入与功能的扩展会发现越来越难以维护,或许这也是很多用不习惯JS的程序员不喜欢 JS的一个原因 之一吧。
async1(function(input, result1) {
async2(function(result2) {
async3(function(result3) {
async4(function(result4) {
async5(function(output) { // do something with output });
});
});
});
})
以上这种代码便是我们可以在某些程序员同学中可以看到过的代码,特别是操作数据库部份经常容易出现这样的代码。以前一个同事说用过Node.js开发过一次程序后就不想再次开发程序,因为已经受够了这种难以维护的代码。当时我向他推荐了一些类似如Asyncjs,Step之类的流程控制的库,最后那个项目是否采用就不得而知。
巢式风格的代码 可能引起的问题
解决之道
目前社区有三种观点:
(一)
将代码分离再分离。虽然还是有存在回调函数的问题,但是代码的复杂度得到一定的降低。举例而言(以nodejs为例,但异步流程控制问题并不是NODEJS的专利):
var http=require(‘http’);
http.createServer(function(request,response){
response.writeHead(200,{‘Content-Type’:‘text/plain’});
response.write(‘hello’);
response.end();
}).listen(80,’127.0.0.1’);
上面代码看似没有什么问题,但是如果这个时候我要向浏览器端返回一个文件,如果文件没存在返回404。又如果我要增加数据库访问功能。如果代码全放到这个里面。那维护起来会是怎么样的情况!
var http=require(‘http’);
function requesthander(req,res){
res.writeHead(200,{‘Content’,’text/plain’});
res.wirte(‘hello’);
res.end();
}
http.createServer(requesthander).listen(80,’127.0.0.1’);
这种分离方法被早期的一些设计所使用,其主体思想是将巢状代码给展平,分离每一步逻辑。但是这个也带来了另外的问题。虽然实现了分享,但是代码之间过于松散,代码之间的关系必须层层深入的去理解。
(二)
将代码进一步抽象,使用统一的库或工具包去组织代码,不破坏原有代码的逻辑与组织。就Javascript 世界来说出现了很多优秀的库。如Setp,Async(如网易开源的游戏开发框架)之类的库,他们提供了丰富的流控制接口,利用这些接口方法你可以很好的组织或利用好你的代码,减轻开发的负担。
使用方式如下:
async.series(
[
function(callback) {
callback(null, ‘Node.js’);//第一个函数的执行结果传给下个
},
function(callback) {
callback(null, ‘JavaScript’);//第二个函数的执行结果在传给最后的回调函数
},
],
function(err, response) {
// response is [‘Node.js’, ‘JavaScript’] 最后的结果收集。
}
);
三)
编译执行。是的,其大概思路是将已有的代码再通过某种方式编译一式,最终实现异步的功能。比如国内老赵的Jscex.虽然本人不太喜欢这种风格的方试,但是还是向大家简单介绍一下其代码风格。Eval的使用以及代码组织方式让我放弃了使用此库。
下面开始此次的正题,介绍社区中常用的三种js异步流程控制模式
无论网络上的代码怎么变化,提供了怎么样的接口,但目前来说仍旧脱离不开以下三种模式。希望通过简单的介绍能让您也写出一个自己的异步流程控制库或能了解其基本原理也不错。
Series(串行模式)
-------每次只执行一个任务。
Fully parallel(全并发模式)
-------一次性全部执行
Limitedly parallel(限制并发模式)
-------每次按限制的个数执行并发任务。
异步处理库需要具备的功能
1、能够提供控制函数集执行的方法。
2、能够收集执行过程中所产生的数据。
3、提供特殊情况下的并发限制,因为在某些情况下对于并发数量是有控制的,否则可能会让你的系统吃不消。
4、能够提供方法能在所有函数集确认执行成功后执行。
Series模式
适用场景:对执行顺序有具体要求的情景。
如查询多条数据库且数据库语句有依赖情况下
又比如说写文件(目录权限,文件是否存在,文件权限);
特点:
按顺序执行函数集
在运行时每次只运行一个任务
确保每次运行都在上一个任务完成后执行。
每一步的数据是可控,可在下一个任务执行时被调用到(数据顺序性)。
Fully parallel模式
适用场景:并发运行多个任务,当所有任务运行完通知回调方法。如多个I/O操
作并且每个时长不确定的时候,可能需要这种模式。
特点:
并发运行多个任务,不用等待上一个任务执行完才执行。
运算执行只能在回调函数中得到,因为并发模式不能确保执行的顺序。
Limited parallel模式
适用场景:系统资源有限,且每个执行任务消耗资源较多。
特点:
并发运行多个限定任务,不用等待上一个任务执行完才执行。
运算执行结果只能在回调函数中得到,因为并发模式不能确保执行的顺序。
附三种模式的代码:、
/**
* Series flow
*@param {Array} callbacks
*@param {Function} last
*@example series([fun1(next){
someFun();
next();//or next(value)
},fun2(next){
someFun();
next();//or next(value)
}])
**/
function series(callbacks, last) {
var results = [];
function next() {
var callback = callbacks.shift();
if(callback) {
callback(function() {
results.push(Array.prototype.slice.call(arguments));
next();
});
} else {
last(results);
}
}
next();
}
// Example task
function async(arg, callback) {
var delay = Math.floor(Math.random() * 5 + 1) * 100; // random ms
console.log('async with \''+arg+'\', return in '+delay+' ms');
setTimeout(function() { callback(arg * 2); }, delay);
}
function final(results) { console.log('Done', results); }
series([
function(next) { async(1, next); },
function(next) { async(2, next); },
function(next) { async(3, next); },
function(next) { async(4, next); },
function(next) { async(5, next); },
function(next) { async(6, next); }
], final);
--------------------------------------------------------
function fullParallel(callbacks, last) {
var results = [];
var result_count = 0;
callbacks.forEach(function(callback, index) {
callback( function() {
results[index] = Array.prototype.slice.call(arguments);
result_count++;
if(result_count == callbacks.length) {
last(results);
}
});
});
}
// Example task
function async(arg, callback) {
var delay = Math.floor(Math.random() * 5 + 1) * 100; // random ms
// console.log('async with \''+arg+'\', return in '+delay+' ms');
for(var i =0;i<100000000;i++){
}
setTimeout(function() { callback(arg * 2); }, delay);
console.log(+new Date());
}
function final(results) { console.log('Done', results); }
fullParallel([
function(next) { async(1, next); },
function(next) { async(2, next); },
function(next) { async(3, next); },
function(next) { async(4, next); },
function(next) { async(5, next); },
function(next) { async(6, next); }
], final);
-------------------------------------------------
function limited(limit, callbacks, last) {
var results = [];
var running = 1;
var task = 0;
function next(){
running--;
if(task == callbacks.length && running == 0) {
last(results);
}
while(running < limit && callbacks[task]) {
var callback = callbacks[task];
(function(index) {
callback(function() {
results[index] = Array.prototype.slice.call(arguments);
next();
});
})(task);
task++;
running++;
}
}
next();
}
// Example task
function async(arg, callback) {
var delay = Math.floor(Math.random() * 5 + 1) * 1000; // random ms
console.log('async with \''+arg+'\', return in '+delay+' ms');
setTimeout(function() {
var result = arg * 2;
console.log('Return with \''+arg+'\', result '+result);
callback(result);
}, delay);
}
function final(results) { console.log('Done', results); }
limited(2, [
function(next) { async(1, next); },
function(next) { async(2, next); },
function(next) { async(3, next); },
function(next) { async(4, next); },
function(next) { async(5, next); },
function(next) { async(6, next); }
], final);
---------------------------------------------
参考文献 :http://book.mixu.net/
================================================