非阻塞的io模型
首先,io操作无疑是耗时的,当服务器端接收到大量请求时,为每一个请求创建进程或线程的同时,也增加了额外的内存开销,也可能浪费更多的时间资源。
由于node.js是事件驱动的,于是它使用了事件循环来解决io操作带来的瓶颈问题。在node.js中,一个io操作通常会带有一个回调函数,当io操作完成并返回时,就会调用这个回调函数,而主线程则继续执行接下来的代码。简单的用一个例子来说明这个问题:
事件循环 event loop
接下来,来讨论下事件循环的机制。首先说说调用桟,比如有如下一段代码:
当代码执行后,函数a首先被推入调用桟中成为栈顶元素并开始执行a,在执行过程中函数b又被推入调用桟成为栈顶元素,在b执行完成后,b被弹出调用桟,a再次成为栈顶元素,在a执行完成后a被弹出调用桟,调用桟呈空闲状态。
在javascript运行时中存在一个消息队列,而消息和一个回调函数相关联,当一个事件被触发时,如果这个事件有相应的回调函数,则该消息就会被加入到消息队列中去。
回过头来说事件循环到底循环的是什么,在代码开始执行后,函数被不断推入调用桟中,就拿上面的例子来讲,request被推入调用桟中,这个函数将进行一个http请求(这个http请求将交由node.js的底层模块来实现)同时请求完成的事件和一个回调函数关联起来,request被弹出调用桟,console.log被推入调用桟开始执行。当请求完成时,完成事件被触发,一条消息被添加进消息队列中,消息队列首先会检查调用桟是否为空闲状态,如果调用桟并不空闲,则会一直等待到调用桟空闲状态后,将消息队列的头部弹出,此时与该消息相关联的回调函数被执行。
小结
以上就无阻塞模型和事件循环在概念上进行了总结。而这个事件循环的机制并不仅仅是node.js所独有的,并且node.js的代码是单线程执行的,在面对大量并发请求的时候,又有着什么优势呢?
上面这张图展示了node.js的架构图,node.js的底层有一个模块负责维护线程池,当一个io请求发出的时候,node.js的底层模块将新建一个线程来处理请求,完成后再将结果交还给上层。那么,当有多个请求的时候,node.js的底层模块将利用尽可能少的线程来完成最多的任务,如果存在空闲的线程,它将继续被利用来做其他的事情,这对于前面说的针对每个请求开一个新的进程或线程而言,无疑“聪明”许多,也更加高效了。