AsynchronousI/OAsynchronousI/OandNon-blockingI/O从计算机内核I/O的角度来看,synchronous/asynchronous和blocking/non-blocking是两种不同的概念。操作系统内核只有两种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事件循环会在进程启动时创建一个类似于while(true)的循环。每次执行循环体的过程我们称之为tick。在每个tick期间,观察者用于确定是否有需要处理的事件。整个异步I/O流程事件循环、观察者、请求对象、线程池共同构成了Node异步I/O的基本要素。非I/O异步API当我们介绍Node的时候,大多数情况下都会提到异步I/O,但是在Node中也会有一些与I/O无关的异步API。比如setTimeout()、setInterval()、setImmediate()、process.nextTick()。
