异步I/O
异步I/O与非阻塞I/O
从计算机内核I/O而言,同步/异步和阻塞/非阻塞是两个不同的概念。
操作系统内核对于I/O只有两种方式,阻塞和非阻塞。
阻塞I/O的特点是调用之后一定要等到系统内核层面完成所有操作后,调用才结束,阻塞I/O造成了CPU等待I/O,浪费了CPU的处理能力。非阻塞I/O与阻塞I/O的区别是调用之后会立即返回。
非阻塞I/O返回后,CPU可以用来处理其他事情,此时性能提升是明显的。非阻塞I/O返回时,并没有获取到完整的数据,应用程序需要重复调用I/O操作来确认是否完成,这种重复调用判断操作是否完成的技术叫做轮询。
阻塞I/O造成CPU等待浪费,非阻塞I/O需要轮询判断数据是否获取完毕,也会浪费CPU资源。
现存的轮询技术
- read
它是最原始,性能最低的一种,通过重复调用来检查I/O的状态来完成数据的读取。在得到数据之前CPU一直耗在等待上。 - select
它在read的基础上进行了改进,通过对文件描述符上的事件状态进行判断。但是由于它采用1024长度的数组存储状态,所以它最多可以同时检查1024个描述符 - poll
该方案较select有所改进,采用链表的方式避免数组长度的限制,其次能避免不需要的检查。 - epoll
这是Linux下效率最高的事件通知机制。在进入轮询的时候如果没有检查到I/O事件,将会进行休眠,直到事件发生将它唤醒。它真实利用了事件通知执行回调的方式。尽管该方式利用事件降低CPU的消耗,但是休眠期间CPU几乎是闲置的,对于当前线程而言CPU利用率不够。 - kqueue
实现方式类似epoll,只在FreeBSD里存在。
理想的非阻塞异步I/O
我们期望的完美异步I/O应该是应用程序发起非阻塞调用,无需通过遍历或者事件唤醒等方式轮询,直接处理下一个任务,只需在I/O完成之后通过信号或者回调将数据传递给应用程序。
在Linux中存在这样一种方式AIO,它通过信号或回调传递数据,不幸的是,它只在Linux中有,并且它只支持内核I/O中的O_DIRECT方式读取,导致无法利用系统缓存。
现实的异步I/O
前面的场景限定在单线程情况下,在多线程情况下,可以让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来完成数据获取,通过线程之间的通信将数据进行传递。
因此可以通过线程池模拟异步I/O,像libeio就是通过线程池和阻塞I/O来模拟异步I/O。
最初Node就在Linux平台上使用该库实现了异步I/O,后来自行实现了线程池来实现异步I/O。
Windows下的异步I/O方案是IOCP,内部也是线程池的原理。
由于Windows平台和Linux平台的差异,Node提供libuv最为抽象封装层,保证上层Node和下层自定义线程池和IOCP之间各自独立。
我们时常提到Node是单线程的,这里的单线程仅仅是javascript执行在单线程中,在Node中,无论是Windows还是Linux,内部完成I/O任务时另有线程池。
Node的异步I/O
事件循环
在进程启动是Node会创建一个类似于while(true)的循环。每执行一次循环体的过程我们称之为tick。在每个tick过程中,通过观察者来判断是否有事件需要处理。
整个异步I/O流程
事件循环,观察者,请求对象,线程池共同构成了Node异步I/O的基本要素。
非I/O的异步API
我们在介绍Node的时候,多数情况下都会提到异步I/O,但是Node中还会存在一些与I/O无关的异步API。比如setTimeout(),setInterval(),setImmediate(),process.nextTick()。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。