NodeJs的异步I/O

目前的开发过程中,都是单线程的,所以当两个请求发起的时候,一个在请求,另一个就必须等待上一个请求完成后才能开始自己的请求,这样就大大的浪费了时间,而且在用户体验上也非常糟糕。

NodeJs的异步I/O解决了什么问题?

  1. 用户体验
  2. 资源分配
  3. 异步I/O实现现状
  4. 异步I/O与非阻塞I/O
  5. 理想的非阻塞异步I/O
  6. 事实的异步的I/O

Node的异步I/O

  1. 事件循环
  2. 观察者
  3. 请求对象
  4. 执行回调

Node的事件循环

在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们。然后进入下个循环,如果不再有事件处理,就退出进程。

Node的观察者

在每个Tick的过程中,如何判断是否有事件需要处理呢?这里必须要引入的概念是[观察者]。每个事件循环中有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。

事件可能来自用户的点击或者加载某些文件时产生,而这些产生的事件都有对应的观察者。在Node中,事件主要来源于网络请求、文件I/O等,这些事件对应的观察者有文件I/O观察者、网络I/O观察者等。观察者将事件进行了分类。

事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。

请求对象

对于一般的(非异步)回调函数,函数由我们自行调用,而对于Node中的异步I/O调用而言,回调函数却不由开发者来调用。那么从我们调用后,到回调函数被执行,中间发生了什么呢?事实上,从JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,它叫做请求对象。

执行回调

组装好请求对象、送入I/O线程池等待执行,实际上完成了异步I/O的第一部分,回调通知是第二部分。

线程池中的I/O操作调用完毕后,会将获取的结果储存在req->result属性上,然后调用PostQueuedCompletionStatus()通知IOCP,告知当前对象操作已经完成:

1
PostQueuedCompletionStatus((loop)->iocp,0,0,&((req)->overlapped))

PostQueuedCompletionStatus()方法的作用是向IOCP提交执行状态,并将线程归还线程池。通过PostQueuedCompletionStatus()方法提交的状态,可以通过GetQueuedCompletionStatus()提取。

在这个过程中,我们其实还动用了事件循环的I/O观察者。在每次Tick的执行中,它会调用将请求对象加入到dI/O观察者的队列中,然后将其当做事件处理。

I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,取出oncomplete_sym属性作为方法,然后调用执行,以此达到调用JavaScript中传入的回调函数的目的。至此,整个异步I/O的流程完全结束。

总结

事件循环、观察者、请求对象、I/O线程池这四者共同构成了Node异步I/O模型的基本要素。