由于编程人员阅读思维习惯,同步I/O盛行了很多年,但同步问题会引起性能问题,于是我们开始使用多线程来解决性能问题。
但多线程存在CPU浪费的情况,比如线程之间切换,线程与线程之间锁,同步问题等等,会使CPU存在浪费情况。
下面来看看异步编程的优势与难点。
4.2.1 异步编程的优势
Node带来最大特性莫过于基于事件驱动的非阻塞I/O模型。非阻塞I/O可以使CPU与I/O并不互相依赖等待,让资源得到更好的利用。
由于事件循环模型需要对应海量请求,所有请求会作用于单线程上,需要防止任何一个计算耗费过多的CPU时间片。至于计算密集型,还是I/O密集型,只要计算不影响异步I/O的调度,那就不构成问题。
建议对CPU的耗用不要超过10ms,或者将大量的计算分解为诸多的小量计算,通过setImmediate()进行调度。只要合理利用Node的异步模型与V8的高性能,就可以充分发挥CPU和I/O资源的优势。
4.2.2 异步编程的难点
1.异常处理
过去我们处理异常时,通常使用try/catch/final语句块进行异常捕获,代码如下:
try{
JSON.parse(json);
}catch(e){
//TODO
}
但这对于异步编程而言并不适用。
之前文章提到,异步I/O的实现主要包含两个阶段:提交请求和处理结果。这两个阶段中间有事件循环的调度,两者彼此不关联。通常在第一步提交后,立马就返回了。
异步方法的定义如下:
var async = function (callback) {
process.nextTick(callback);
}
进行如下的try/catch操作:
try{
async(callback);
}catch(e){
// TODO
}
上述代码是无法对callback的异常进行捕获。
2.函数嵌套过深
例如在事务中存在多个异步调用的场景比比皆是。比如一个遍历目录的操作,其代码如下:
fs.readdir(path.join(__dirname, '..'), function (err, files) {
files.forEach(function (filename, index) {
fs.readFile(filename, 'utf8', function (err, file) {
// TODO
});
});
});
对于上述场景,由于两次操作存在依赖关系,函数嵌套的行为也许情有可原。但是如果在readFile内再进行各种回调,那么就有些难以接受。
3.阻塞代码
对于刚入Javascript不久的开发者而言,该语言竟然没有sleep()这样的线程操作,唯独有setInterval()和setTimeout()这两个函数,而且这两个函数并不会阻塞后续代码持续执行。
4.多线程编程
Javascript是单线程的,单个进程无法充分利用多核CPU。但Node也提供了多进程的功能,后续章节会提到。