“异步”这个名词的大规模流行是在Web 2.0浪潮中,它伴随着Javascript和AJAX席卷了Web。但在绝大多数高级编程语言中,异步并不多见。PHP最能体现这个特点:它不仅屏蔽了异步,甚至连多线程也不提供,PHP都是以同步阻塞的方式来执行。这样的优点利于程序猿顺序编写业务逻辑,但在复杂的网络应用中,阻塞导致它无法更好地并发。
在服务器端,I/O非常昂贵,分布式I/O更加昂贵,只有后端能快速响应资源,前端的体验才能变得更好。Node.js是首个将异步作为主要编程方式和设计理念的平台,伴随着异步I/O的还有事件驱动和单线程,它们构成Node的基调。本文将介绍Node是如何实现异步I/O的。
1. 基本概念
“异步”与“非阻塞”听起来似乎是一回事,从实际效果而言,这两者都达到了并行的目的。但是从计算机内核I/O而言,只有两种方式:阻塞与非阻塞。因此异步/同步和阻塞/非阻塞实际上是两回事。
1.1 阻塞I/O与非阻塞I/O
阻塞I/O的一个特点是调用之后一定要等到系统内核层面完成所有操作后,调用才结束。以读取磁盘上的一个文件为例,系统内核在完成磁盘寻道、读取数据、复制数据到内存中后,这个调用才结束。
阻塞I/O造成CPU等待I/O,浪费等待时间,CPU的处理能力不能得到充分利用。非阻塞I/O的特点就是调用之后会立即返回,返回后CPU的时间片可以用来处理其他事务。由于完整的I/O并没有完成,立即返回的并不是业务层期待的数据,而仅仅是当前调用的状态。为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成(即轮询)。轮询技术要以下几种:
1.read:通过重复调用来检查I/O状态,是最原始性能最低的一种方式
2.select:对read的改进,通过对文件描述符上的事件状态来进行判断。缺点是文件描述符最大的数量有限制
3.poll:对select的改进,采用链表的方式避免最大数量限制,但描述符较多时,性能还是十分低下
4.epoll:进入轮询时若没有检查到I/O事件,将会进行休眠,直到事件发生将其唤醒。这是当前Linux下效率最高的I/O事件通知机制
轮询满足了非阻塞I/O确保获取完整数据的需求,但对于应用程序而言,它仍然只能算作一种同步,因为依然需要等待I/O完全返回。等待期间,CPU要么用于遍历文件描述符的状态,要么用于休眠等待事件发生。
1.2 理想与现实中的异步I/O
完美的异步I/O应该是应用程序发起非阻塞调用,无需通过轮询就可以直接处理下一个任务,只需在I/O完成后通过信号或回调将数据传递给应用程序即可。
现实中的异步I/O在不同操作系统下有不同的实现,如*nix平台采用自定义的线程池,Windows平台采用IOCP模型。Node提供了libuv作为抽象封装层来封装平台兼容性判断,并保证上层Node与下层各平台异步I/O的实现各自独立。另外需要强调的是我们经常提到Node是单线程的,这仅仅是指Javascript的执行在单线程中,实际在Node内部完成I/O任务的都另有线程池。









