单线程编程会因阻塞I/O导致硬件资源得不到更优的使用。多线程编程也因为编程中的死锁、状态同步等问题让开发人员头痛。Node在两者之间给出了它的解决方案:利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以好使用CPU。
异步IO的实现
在Node中,JS是在单线程中执行的没错,但是内部完成IO工作的另有线程池,使用一个主进程和多个IO线程来模拟异步IO。
当主线程发起IO调用时,IO操作会被放在IO线程来执行,主线程继续执行下面的任务,在IO线程完成操作后会带着数据通知主线程发起回调。
事件循环
这是Node的执行模型,正是这种模型使得回调函数非常普遍。
在进程启动时,Node便会创建一个类似while(true)的循环,执行每次循环的过程就是判断有没有待处理的事件,如果有,就取出事件及其相关的回调并执行他们,然后进入下一个循环。如果不再有事件处理,就退出进程。
观察者
在每个循环中,怎么判断是否有事件需要处理呢?这里就要引入观察者了。每个事件循环中都有一个或多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。
事件循环是一个典型的生产者/消费者模型,异步IO,网络请求等是事件的生产者,源源不断的为Node提供不同类型的事件,这些事件被传递到观察者那里,事件循环则从观察者那里取出事件并处理。
请求对象
对于Node中的异步IO调用而言,回调函数不由开发者来调用,从JS发起调用到IO操作完成,存在一个中间产物,叫请求对象。
在JS发起调用后,JS调用Node的核心模块,核心模块调用C++内建模块,內建模块通过libuv判断平台并进行系统调用。在进行系统调用时,从JS层传入的方法和参数都被封装在一个请求对象中,请求对象被放在线程池中等待执行。JS立即返回继续下面的操作。
执行回调
在线程可用时,线程会取出请求对象来执行IO操作,执行完后将结果放在请求对象中,并归还线程。
在事件循环中,IO观察者会不断的找到线程池中已经完成的请求对象,从中取出回调函数和数据并执行。
流程图
非IO的异步API
定时器
setTimeout()和setInterval()
这两个方法实现原理与异步IO相似,只不过不用线程池的参与。
使用它们创建的定时器会被放入定时器观察者中,每次事件循环执行时会从观察者中取出并判断是否超过定时时间,超过就形成一个事件,回调立即执行。
所以,和浏览器中一样,这个并不精确。
process.nextTick()
有时我们想要立即异步执行一个任务,可能会使用延时为0的定时器,但是这样开销很大。我们可以换而使用这个,这个会将传入的回调放入队列中,下一轮Tick(事件循环)中取出执行。
process.nextTick(function(){console.log("nextTick");});
console.log("thisTick");
setImmediate()
这个函数表现上与process.nextTick()相同,但是还是有细微的区别
当setImmediate()遇上process.nextTick()时,process.nextTick()的优先级高于setImmediate(),这里是因为事件循环对于观察者的检查是有顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者。
idle观察者优于IO观察者优于check观察者。
而且,对于process.nextTick()的回调函数,是保存在一个数组中的,当有多个时,会在下一个Tick全部执行完,而setImmediate()的回调函数们在一个链表中,每轮Tick只执行一个。这样设计是为了防止一次循环持续过久,CPU过多占用。(这个特性新版本好像取消了,也是一次循环执行完,可以用下面的例子测试下)。
process.nextTick(function () {
console.log('nextTick执行1');
});
process.nextTick(function () {
console.log('nextTick执行2');
});
setImmediate(function () {
console.log('setImmediate执行1');
process.nextTick(function () {
console.log('势入');
});
});
setImmediate(function () {
console.log('setImmediate执行2');
});
console.log('正常执行');
事件驱动与服务器
网络IO事件也同样的应用到了异步IO。网络上的请求都会交给IO观察者来处理。
经典的服务器有下面几种:
同步式
每进程每请求
每线程每请求
上面的模型都有在大量请求下性能下降的问题。
Node通过事件驱动来处理请求,每个请求不必创建新的线程,开销小。