异步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()。


HPJL
7 声望0 粉丝