同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
node js的异步I/O是它的一个重要功能,为了讲清楚这个机制,先说一下操作内核对于I/O的处理方式:阻塞I/O和非阻塞I/O
阻塞I/O的特点是调用之后一定要等到这个I/O的所有动作都完成后,调用才结束返回。在这期间,CPU的其它所有运算都要停滞,因此阻塞I/O使CPU等待I/O,浪费了CPU资源。
非阻塞I/O的特点就是调用之后,立即结束返回,这样,CPU就不会因为要等待I/O结束而浪费计算资源。但是,由于非阻塞I/O会立即返回,返回时完整的I/O过程并没有完成,所以返回的不是期望的I/O数据,而仅仅是当前调用的状态。所以,为了获取期望的数据,系统就要通过轮询去确定这个I/O是否完成。
从以上的描述可以看出,阻塞I/O需要系统等待I/O完成,非阻塞I/O则需要系统去轮询这个I/O是否完成,在这个层面node js的异步I/O更接近于非阻塞I/O。确切地说,node js是在非阻塞I/O的基础上,用更高效的方法代替轮询这个机制,从而达到了它称之为的异步I/O的功能。下面介绍几种不同的轮询方式:
直接轮询:它是最原始、效率最低的一种轮询方式,它通过重复调用来检查I/O的完成状态。
1 2 3 4 5 6 | while true { for i in stream[]: { if i has data read until unavailable } } 爱创课堂--专业前端技术培训 年薪30万不是梦 |
select:比较直接轮询,select使用了一个代理来检测多个I/O的状态,如果I/O还没完成,就会阻塞掉这个处理I/O的线程,使得CPU通过调度处理别的线程,当有I/O完成时,再唤醒这个线程,让程序轮询一遍所有的I/O流,找到完成的I/O并进行下一步的处理。
1 2 3 4 5 6 7 | while true { select(streams[]) for i in streams[] { if i has data read until unavailable } } |
epoll:相比于select,epoll能把那些I/O完成了哪些事件也通知给我们,因此程序就不需要再轮询一遍所有的I/O流了。
1 2 3 4 5 6 7 | epollfd = epoll_create() while true { active_stream[] = epoll_wait(epollfd) for i in active_stream[] { read or write till unavailable } } |
node js异步I/O:
异步I/O理想的状态就是应用程序发起非阻塞调用,无序通过遍历或者事件唤醒等方式轮询,直接处理下一个任务,只需在I/O完成后通过信号或者回调函数将数据传递给应用程序即可。为了实现这个功能,node采用了线程池和回调函数这两个技术。
首先,node里有一个观察者,和一个采用生产者/消费者的模型的事件循环,各种I/O请求作为生产者被传递到观察者那里,然后事件循环从观察者出取出事件,进行下一步处理。
事件循环里取出的事件,必须伴随着事件完成的回调函数
1 2 3 4 | fs.open=(path,flags,mode,callback){ ... callback() } |
在取出事件之后,node立即返回,执行当前任务的后续任务。而这个I/O时间和它的回调函数一起,被打包送入系统的线程池,然后后续的I/O将由线程池中的线程处理。
最后,node的观察者会提取线程池中完成的I/O获得的数据,并调用它返回的回调函数,来执行整个I/O的回调。至此,node js的异步I/O结束。其中,我认为node js异步I/O最大的特点就是回调函数是由操作系统触发的,而不是由程序触发的。