深入浅出Node

236 阅读6分钟

第一章、node简介

1、chrome浏览器除了V8作为JavaScript引擎外,还有一个WebKit布局引擎。

2、单线程的弱点有以下3个方面

(1)无法利用多核CPU。

(2)错误会引起整个应用退出,应用的健壮性值得考验。

(3)大量计算占用CPU导致无法继续调用异步I/O。

3、浏览器中,Web Workers能够创建工作线程来进行计算,以解决JavaScript大计算阻塞UI渲染的问题。工作线程为了不阻塞主线程,通过消息传递的方式来传递结果,这也使得工作线程不能访问主线程中的UI。

4、Node采用了与Web Workers相同的思路来解决单线程中大量计算的问题:child_process。子进程的出现,意味着node可以从容地应对单线程在健壮性和无法利用多核CPU方面的问题。通过将计算分发到各个子进程,可以将大量计算分解掉,然后再通过进程之间的事件消息来传递结果,这可以很好地保持应用模型的简单和低依赖。

5、Node面向网络且擅长并行I/O,能够有效地组织起更多硬件资源,从而提供更多好的服务。

6、适当调整和分解大型运算任务为多个小任务,使得运算能够适当释放,不阻塞I/O调用的发起,这样既可以同时享受到并行异步I/O的好处,又能充分利用CPU。

7、对于长时间的运算,如果它的耗时超过普通阻塞I/O的耗时,那么运用场景就需要重新评估

8、Node虽然没有提供多线程用于计算支持,但是还是有以下两个方式来充分利用多核CPU:

   (1)Node可以通过编写C/C++扩展的方式更高效地利用多核CPU,将一些V8不能做到性能极致的C/C++来实现。

(2)如果单线程的Node不能满足需求,甚至用了C/C++扩展后还是觉得不够,那么通过子进程的方式,将一部分Node的进程当作常驻服务进程用于计算,然后利用多进程的消息来传递结果,将计算与I/O分离,这样还能充分利用CPU。

第二章、模块机制

1、与浏览器会缓存静态脚步文件以提高性能一样,Node会对引入过的模块都会进行缓存,以减少二次引用的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象。

2、事实上,在编译过程中,Node对获取的JavaScript文件进行了头尾包装。在头部添加了(function(exports, require, _filename, _dirname){\n,在尾部添加了\n};。一个正常的JavaScript文件会被包装成如下的样子:

(function(exports, require, _filename, _dirname){
  var math = require('math');
  exports.area = function(radius){
    return Math.PI * radius * radius;
  };
});

这就是为什么js中存在require、exports、module这三个变量的原因

第三章、为什么要异步I/O

1、在浏览器中JavaScript在单线程上执行,而且它还与UI渲染共用一个线程。这意味着JavaScript在执行的时候UI渲染和响应是处于停滞状态的,如果脚步的执行时间超过100毫秒,用户就会感到页面卡顿,以为网页停止响应。

2、同步I/O的时间消耗为M+N+...,异步为max(M,N,...)。随着网站或应用不断膨胀,数据将会分布到多台服务器上,分布式将会是常态。分布式也意味着M与N的值会线性增长,这也放大异步和同步在性能方面的差异。I/O是昂贵的,分布式I/O是更昂贵的,只有后端能够快速响应资源,才能让前端的体验更好。

3、单线程同步编程模型会因阻塞I/O导致硬件资源得不到更优的使用。多线程编程模型也因为编程中的死锁、状态同步等问题让开发人员头疼。Node利用单线程,远离多线程死锁,状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好的使用CPU。

4、为了弥补单线程无法利用多核CPU的缺点,Node提供了类似浏览器中Web Workers的子进程,该子进程可以通过工作进程高效地利用CPU和I/O。

5、阻塞I/O造成CPU等待浪费,非阻塞带来的麻烦却是需要轮询去确认是否完成数据获取,它会让CPU处理状态判断,是对CPU资源的浪费。

6、我们时常提到Node是单线程的,这里的单线程仅仅只是JavaScript执行在单线程中罢了。在Node中,无论*nix还是Window平台,内部完成I/O操作的另有线程池。

7、Node中还存在一些与I/O无关的异步API,分别是setTimeout(),setInterval()、setImmediate()、process.nextTick()。

8、setTimeout()和setInterval()的实现原理与异步I/O类似,只是不需要I/O线程池的参与。调用setTimeout()或者setInterval()创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次Tick时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就形成一个事件,它的回调函数将会立即执行。

9、采用定时器需要动用红黑树,创建定时器对象和迭代等操作,而setTimeout(fn, 0)的方式较为浪费性能。实际上,process.nextTick()方法的操作相对较为轻量,其只会将回调函数放入队列中,时间复杂度较低。

10、process.nextTick()中的回调函数的优先级要高于setTmmediate()。这里的原因在于事件循环对观察者的检查是有先后级关系的,process.nextTick()属于idle观察者,setImmediate()属于check观察者。在每一轮循环中,idle观察者先于I/O观察者,I/O观察者优于check观察者。

11、在具体的实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。在行为上,processa.nextTick()在每轮循环中会将数组中的回调函数全部执行完,而setImmediate()在每轮循环中执行链表的一个回调函数。

12、事件循环是异步实现的核心。

第四章、异步编程