1 用户层面(语言层面)定义的异步I/O
在操作系统中,程序运行的空间分为内核空间和用户空间
。我们常常提起的异步I/O,其实质是用户空间中的程序不用依赖内核空间中的I/O操作实际完成,即可进行后续任务
。以下伪代码模仿了一个从磁盘上获取文件和一个从网络中获取文件的操作。异步I/O的效果就是getFileFromNet的调用不依赖于getFile调用的结束
。
getFile("file_path");
getFileFromNet("url");
如果以上两个任务的时间分别为m和n。采用同步方式的程序要完成这两个任务的时间总花销会是m + n。但是如果是采用异步方式的程序,在两种I/O可以并行的状况下(比如网络I/O与文件I/O),时间开销将会减小为max(m, n)
。
1.1 异步I/O的必要性
有的语言为了设计使得应用程序调用方便,将程序设计为同步I/O的模型。这意味着程序中的后续任务都需要等待I/O的完成。在等待I/O完成的过程中,程序无法充分利用CPU
。为了充分利用CPU,和使I/O可以并行,目前有两种方式可以达到目的:
- 多线程单进程
多线程的设计之处就是为了在共享的程序空间中,实现并行处理任务,从而达到充分利用CPU的效果
。多线程的缺点在于执行时上下文交换的开销较大,和状态同步(锁)的问题
。同样它也使得程序的编写和调用复杂化。 - 单线程多进程
为了避免多线程造成的使用不便问题,有的语言选择了单线程保持调用简单化,采用启动多进程的方式来达到充分利用CPU和提升总体的并行处理能力
。它的缺点在于业务逻辑复杂时(涉及多个I/O调用), 因为业务逻辑不能分布到多个进程之间,事务处理时长要远远大于多线程模式
。
多线程和多进程都是系统控制进程/线程切换,异步则不存在线程/进程切换打破原子性,也就不存在静态条件/同步问题。同步简单的说是,要求同一时刻,只能有一个东西获得某个资源。
前者在性能优化上还有回旋的余地,后者的做法纯粹是一种加三倍服务器的行为
。而且现在的大型Web应用中,单机的情形是十分稀少的,一个事务往往需要跨越网络几次才能完成最终处理。如果网络速度不够理想,m和n值都将会变大, 这时同步I/O的语言模型将会露出其最脆弱的状态
。这种场景下的异步I/O将会体现其优势,max(m, n)的时间开销可以有效地缓解m和n值增长带来的性能问题。而当并行任务更多的时候,m + n + ...与max(m, n, ...)之间的孰优孰劣更是一目了然
。从这个公式中,可以了解到异步I/O在分布式环境中是多么重要,而Node.js天然地支持这种异步I/O,这是众多云计算厂商对其青睐的根本原因。
操作系统层面定义的异步I/O
我们听到Node.js时,我们常常会听到异步,非阻塞,回调,事件这些词语混合在一起。其中,异步与非阻塞听起来似乎是同一回事
。从实际效果的角度说, 异步和非阻塞都达到了我们并行I/O的目的。但是从计算机内核I/O而言,异步/同步和阻塞/非阻塞实际上时两回事
。
同步阻塞模型很简单,异步非阻塞模型则有好几种。
blocking I/O 同步阻塞io
异步非阻塞io模型有这几种。
- nonblocking I/O
- I/O multiplexing (select and poll)
- signal driven I/O (SIGIO)
- asynchronous I/O (the POSIX aio_functions)
其中read,epoll,poll,kqueue,pselect都可以看做一种实现了异步非阻塞目标的轮询模型