Async IO of Node.js
Node和异步IO是什么?
什么是Node
Node是一个Javascript运行环境,它实现了全异步的IO,使Javascript成为了一个通用编程语言。
同步 synchronous
syn = together (syndrome 综合症,syntax 语法:放到一起并保持顺序排列和文法)
chrono = time
synchronous = existing or happening at the same time
不同语境下的“同步”是什么意思?
手机联系人同步,同步电路,同步编程语言,同步IO,同步锁,“画面和字幕不同步啊!”,这些“同步”究竟到底是什么意思?
名,形容词 -> 动词,使…变得同时存在,使…同时发生
- 要理解同步要有时间轴的概念,就如视频的时间轴,画面和字幕播放同时进行,在某一时刻,画面和字幕的表达一致,就是同步的。
- 联系人同步,让同一时刻不一样的两处的联系人变得一样。
- 交响乐演奏中,指挥家协调所有演奏者的演奏步调,不能有人过快和过慢。
- 同步电路,在时钟控制下,同时更新所有路径状态的电路。
异步 = !同步
什么是IO
程序员面对的IO设备只是一些软件接口,可以简单把IO过程看作
IO控制器Buffer <--> 内核Buffer <--> 用户Buffer-->-->
之间的数据流动过程。
不同的角色对于IO的感觉是不一样的
- 对于IO控制器开发者,面对的是逻辑电路、IC接口。
- 对于在内核态编程的开发者(IO驱动开发者),面对的是IO控制器接口。
- 对于在用户态编程的开发者,只需要面对OS的IO接口,open,read,epoll,file system,socket。
- 对于在虚拟机上编程的开发者,面对的是虚拟机提供的IO接口,更加抽象的stream等。
- Node提供了全异步的IO接口。
同步与异步IO
- 是否立即响应?
阻塞、非阻塞IO。
非阻塞IO不配合多路复用的话,不如不用。 - 同步IO,IO请求到响应这段时间,IO调用代码和IO接口都为此次IO操作负责,就像打电话一样,双方同时维持会话,直到通话结束。
- 异步IO,IO请求到响应这段时间,IO调用代码已经不为此次IO操作负责了,就像发短信一样,会话并没有保持。
- 异步IO都是指异步非阻塞IO,不存在异步阻塞IO。
POSIX定义的异步IO要求不能有任何阻塞,但具体实现上都免不了内核空间到用户空间数据拷贝的阻塞,Direct IO还存在实践上的问题,较少使用。
- 异步IO在内核上的实现有Linux 2.6.22+的AIO(应该是最理想的IO,最native的),Windows的IOCP(由内核线程模拟的异步IO,支持socket,文件,pipe)。
- 异步IO在用户态上的实现有Glibc AIO,libeio(由libev作者开发)。
libeio对于sokect,采用的是OS提供的非阻塞IO,对于文件,采用的是阻塞IO,多路复用(epoll等)是不能用于文件的,Linux AIO只适用于文件。
Node使用的libuv是建造在libeio和IOCP之上的,没有使用Linux AIO。
为什么要把Node和异步IO一起谈?
Node强制使用异步IO,使用Node就无法回避异步IO的问题。异步编程具有感染性,会导致关联调用的代码都会变成异步(电路上也是),异步IO的调用会导致几乎所有逻辑都会变成异步逻辑。
事件循环 Event Loop
在同步系统中,事件驱动的实现必然是通过 工作线程 + 事件循环。
可以简单把事件循环看作
while (true) {
/* scan and handle events */
}
的结构,为了保证新产生的事件尽早被发现,handle代码往往会在 工作线程 中异步执行。
可以通过CPU的中断处理来理解事件循环。
外部事件在什么时候都可以设置中断标志
CPU指令周期开始
CPU检查中断标志
如果有中断,进入对应的处理方法
如果没有,CPU继续之前的工作
单线程
Node本身是多线程的
libuv的event loop可以视为主线程
所有的IO请求会被放到线程池执行
Javascript代码会在V8线程中执行
所谓的单线程是指,所有JavaScript永远是在单线程中运行的。
多线程+同步IO 的问题
线程由OS来调度,调度时机不受控制,访问临界资源需要同步控制(前提是多个任务有共享状态的需求)。
就是线程越多,进度状态越多越复杂,同步控制越来越难,加锁越来越难,调试也越来越难。(漏水的桶,补也补不玩)
多线程可能不是应对并发足够好的抽象。
Twisted的作者最初用这种模型来开发游戏(Java),放弃后创造了Twisted。
事件驱动模型能将开发者从处理线程同步维护中解放出来,专注于程序逻辑的开发。
如何调度
JavaScript部分的代码在单线程中运行,只能协作调度,阻塞的js代码会冻结event loop,必须由开发者来控制是否应该让出event loop(nextTick)。
IO调用完全由OS提供的抢占式的调度。保证event loop不会被冻结。
Node一个比较大的问题就在这里,重计算会导致event loop效率底下,并发能力下降。(今后貌似会有协程的加入)
语言特性
JavaScript在语言本身上非常适合异步编程,因为它诞生就为UI编程。
闭包使异步编程体验非常愉悦。stackless
一些有关联的项目的诞生点
1997年,Netscape打算使用Java构建新的浏览器,最终这个项目失败了,Javascript引擎Rhino作为项目遗产而诞生。
2002年,Linux 2.5.44 epoll。
2002年,Java NIO。
2002年,Python Twisted诞生。
2004年,Nginx诞生,C编写的事件驱动的服务器。
2009年,Node诞生。
2011年,Netty诞生,Java NIO + 事件驱动。
2012年,基于Rhino,提供BIO和AIO的Javascript运行环境Ringo诞生。
Node.js的异步IO对我们有什么用?应该如何使用?
我们的目标,快!快!快!
缩短运算时间,增大系统吞吐量,高并发!
调度模型的变化
不再是抢占调度,所以开发者必须注意任务的执行时间不宜太长。别使用阻塞IO
为什么不是一开始就是异步编程
大家在生活中能够轻易处理同步和异步的事情,但是在编程中,往往觉得异步编程更难。
我觉得主要原因是编程学习一开始就是采取同步思维的,汇编,C,Java,多是如此。事件驱动的异步也都是模拟出来的,都是建立在同步编程的基础之上。归根结底还是因为计算机系统一开始就是同步的系统,逻辑电路一开始就是同步时钟驱动的电路。同步编程是根深蒂固的。
如果CPU一开始就是构建在异步电路之上的,或许就是另一番景象了。
异步电路更加难设计。
异步CPU也在设计开发中。
事件驱动难以调试
事件本身就是系统解耦的方式。
一切(绝大部分?)IO都是流
Node对IO做了非常好的stream抽象,stream都可以pipe,stream里可以流buffer,string,object。