深入浅出NodeJS笔记(三)

第3章 异步I/O

阅读了这章内容后,最大的收获是更好地理解了阻塞/非阻塞I/O和Node中异步I/O的含义。

以前,谈到Node的异步I/O机制时,我就简单地理解为非阻塞I/O。实际上,这是不准确的。所谓I/O的阻塞与非阻塞,是操作系统对I/O操作的区分。执行阻塞I/O时,调用要等到所有相关的操作都结束时才算结束。这期间,CPU一直在等待I/O,不能处理其他任务,资源被浪费。执行非阻塞I/O时,调用直接返回,而不是等待I/O相关操作结束。这时,CPU可以处理其他的任务,不会浪费资源。然而,程序需要知道I/O操作何时结束,因此要不停地查询I/O的状态,此为轮询。轮询也会占用CPU资源,尽管有不同的轮询机制实现(如select,poll和epoll机制),优化了CPU资源利用,也只是减少了资源浪费。

注意,这里所说的非阻塞I/O的实现虽然可行,但只是操作系统级别的。对于应用程序来说,如果I/O操作的数据没有完全获取,就不能进行后续操作,因此仍然是一种同步。理想的状态是,应用程序发起一个I/O请求后,不需要执行轮询机制,也可以继续执行后续操作,只需在I/O结束时通过某种机制(消息或回调)将数据交给应用程序即可。这种场景下的I/O操作即使Node要实现的异步I/O

尽管系统不支持(或不能很好支持)异步I/O(因为需要轮询),但是可以通过阻塞I/O和线程池来模拟。使用一部分线程去执行I/O操作,再通过线程间通信机制,将结果传递给主线程。这样I/O操作是在其他线程执行的,并未阻塞主线程的执行,近似(线程需要调度)模拟了异步I/O的场景。Node采用了Windows平台下的IOCP模型实现异步I/O,*nix平台则通过自定义的线程池实现。我们平时所说Node是单线程,指的是Javascript的执行是单线程的,而I/O的操作的处理仍是利用底层系统提供的多线程机制。

接下来看Node异步I/O的具体实现原理。Node在进程启动时会创建一个循环,称为事件循环,每一次循环体执行成为一个tick。在每个tick中,检查是否有事件需要处理,如果有,检查是否有与事件关联的回调函数,如果有就执行它。如果没有事件处理,则退出。

如何判断是否有事件处理呢?Node采用了一系列观察者对象。事件循环通过询问观察者得知是否有事件处理。每个观察者维护者一个事件队列,当有磁盘I/O、网络请求发生时,事件对象被推入观察者队列,而事件循环则从这些队列中取出事件对象。这是一个典型的生产者-消费者模型。观察者对象根据事件的类型可以有多个,比如网络观察者,文件观察者等。另外,事件循环在查看各种观察者的队列时是有一定顺序的,如idle观察者(处理process.nextTick的回调,是非I/O异步调用)先于I/O观察者先于check观察者(处理setImmediate回调,也是非I/O异步调用)。

以Windows下的Node为例,在主程序发起一个I/O操作时,内建模块会通过libuv封装层发起系统调用,这时会创建一个请求对象。该对象封装了Javascript代码传入的参数,当前要执行的函数以及回调函数的信息。然后这个请求对象被推送到线程池等待执行。此时,Javascript主程序调用立即返回,执行后续代码。这时异步I/O的第一个阶段。接下来,当线程池中I/O操作结束后,仍然将结果写入请求对象中,并通知IOCP执行完毕,归还线程。事件循环在每个tick中会询问I/O观察者,后者调用IOCP的API查询是否有执行完的请求,如果有,就将相应的请求对象压入I/O观察者的队列。事件循环得到请求对象后,执行其中的回掉函数。这时异步I/O的第二个阶段。

由Node采用了单线程模型,不会有多线程上下文切换开销较大的问题,这时Node高性能的一个原因。事实上,虽然每个到达的请求都未阻塞后续请求,但请求的处理依然以队列的顺序进行处理,只不过每个请求的处理也不会阻塞。具体处理的过程依然依赖于底层的线程池的性能。

你可能感兴趣的:(深入浅出NodeJS笔记(三))