NodeJS里的异步非阻塞

最近在看《深入浅出nodejs》这本书。刚刚接触node没多久,在用node写一个博客,但还是跃跃欲试地想看看这本在国内被捧得很高的书。看到第三章的时候,异步和同步,阻塞和非阻塞I/O,CPU,事件循环,总感觉自己的知识体系是分散的,不能够串在一块儿。
与其这样,倒不如先梳理一下我知道的那些。

一、异步vs同步

我最早接触到异步这个概念,要追溯到ajax的调用。所谓的同步,即程序按照顺序来执行。但是异步调用打破了这个规则,让执行顺序不按照既定的来。比方说ajax请求。js在发起ajax请求后,程序不会立刻去执行回调。而是继续执行接下来的代码。当某一时刻,请求完成时,才会执行回调。此时,执行顺序早已不是当初的顺序执行。

二、阻塞vs非阻塞i/o

讲到阻塞,就要讲内核和cpu。虽然我们一直把同步等于阻塞,异步等于非阻塞等同于一个概念,但从内核的角度出发,内核调用只有阻塞和非阻塞两种。什么是阻塞i/o?顾名思义,当应用程序发起i/o调用时,会在操作系统层面,即我们的内核去执行i/o操作,后续的代码没办法执行,直到i/o调用结束。这段时间,cpu处于等待状态,如果等待时间过长的话就是对资源的浪费。那么什么是非阻塞i/o?同样很好理解,当我们发起内核调用的时候,非阻塞i/o会立即返回,并执行接下来的代码。但是问题是,立即返回的并不是业务需要的数据,而只是一个文件描述符。想要获取业务需要的数据,需要不停地调用i/o,直到获得数据为止。这个过程我们称之为,轮询。
轮询分为很多种,从最初的不停地调用i/o,到后来文件描述符的事件状态去判断,再到如今依靠事件、回调函数去通知应用程序并且在这期间线程处于休眠,直到事件将其唤醒。轮询的进步,也是非阻塞i/o的进步。最新的轮询技术,cpu的消耗被降到了最低,从一开始不停地发送i/o调用获取数据,导致cpu消耗过大,到如今cpu的合理利用,我们见证了非阻塞i/o的成长。
但是非阻塞也有个问题,表面上看上去变的不再阻塞,但实质上还是在等数据返回,要么让cpu处于等待,要么cpu休眠。不能合理地利用cpu是一个问题。
别灰心,刚才说的只是在单线程的情况下,我们换一个眼光,要是在多线程的环境下,又会是怎样的情况?首先我们让一个线程作为主线程,专门用来处理计算,其余的线程发起i/o调用,不管是非阻塞还是阻塞,不管是轮询还是不轮询,当i/o完成后,以消息的形式,通知主线程,执行回调。这就是线程池原理,在windows下叫iocp。node在最上层和底层中间抽象出一层叫libuv层,i/o内核的调用,就由该层来进行判断。这样cpu既不浪费在等待上,又可以去执行主线程的工作。等i/o完成后又能去执行回调,真是一举两得。

三、i/o vs cpu

在同步(阻塞)i/o调用中,i/o和cpu互斥,两个有一个在执行,另一就等待。但在异步(非阻塞)i/o中,i/o和cpu可以重叠,当发起i/o调用后,cpu可以继续处理接下来的任务。但是不管是阻塞还是非阻塞i/o,cpu在执行的时候,i/o需要等待,所以node的一个缺点,在遇到cpu密集型应用时,后续的i/o没法发起调用,先前的回调也没法处理。node没办法发挥它的优势,即异步非阻塞i/o。它适合于i/o密集型场景。

四、事件循环

我们知道node是基于事件驱动的,事件循环是整个node运作的核心。当有网络请求,或者i/o文件操作时,它们都会以事件的形式加入到事件循环里,让事件循环去处理业务逻辑。而类似于apache服务器的做法则是,来一个网络请求,就开一个线程,来得越多,线程开得也就越多,系统资源最终会被耗尽。所以这也是为什么,如今流行事件驱动的原因,对于系统资源的合理利用是一大特色,乃至ngix服务器也开始采用node这种思想。讲到事件循环就不得不讲观察者(监听者)和请求对象。我们知道,一个事件的产生一定有一个观察者和一个发送者。什么叫事件?消息的载体叫事件。以fs.open(‘filename’, listner)函数为例,回调函数listner就是观察者,fs就是发送者,当内核完成i/o操作后,立即以消息的形式通知观察者,并把数据给到观察者,告诉观察者有事件要执行。当下一次事件循环来临时,会依次遍历事件循环里的观察者们,如果观察者们有数据,就去执行。而这里的数据是怎么给到观察者的,就是我们接下来要说的请求对象的作用。当应用程序发起一次i/o调用,在libuv层会判断是哪个平台的,并且会封装一个请求对象,回调函数作为该请求对象中的一个属性值,并把请求对象推入线程池中去执行。当执行完成之后,数据会以回调函数参数的形式存在,观察者就被通知到了有事件要执行。当下一次tick来临时,事件循环依次挨个遍历观察者,从观察者中取出回调执行。这就是整个异步i/o的流程。nodejs异步i/o流程图
注:回调函数就是观察者(监听者),当监听者监听到有事件要执行,指的就是要去执行回调函数。什么情况下观察者会知道有事件要执行,即执行回调函数?答案就是当内核i/o完成后,数据会以请求对象回调函数的参数的形式存在。

总结

到这儿,不知不觉我们已经把nodejs整个运作的核心部分给梳理了一遍,从同步到异步,从阻塞到非阻塞,从i/o到cpu,再到事件循环,整个nodejs就是围绕着这些概念所展开的。不知不觉知识体系好像完整了起来。其实学习最好的方法,就是反思。写博客是一个很好的反思过程,多写多思会让我们学到很多。

avatar

chilihotpot

You Are The JavaScript In My HTML