Node.js异步I/O学习笔记


Posted in Javascript onNovember 04, 2014

“异步”这个名词的大规模流行是在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任务的都另有线程池。

2. Node的异步I/O

2.1 事件循环

Node的执行模型实际上是事件循环。在进程启动时,Node会创建一个无限循环,每一次执行循环体的过程成为一次Tick。每个Tick过程就是查看是否有事件等待处理,如果有则取出事件及其相关的回调函数,若存在关联的回调函数则执行它们,然后进入下一个循环。如果不再有事件处理,就退出进程。

2.2 观察者

每个事件循环中有若干个观察者,通过向这些观察者询问来判断是否有事件要处理。事件循环是一个典型的生产者/消费者模型。在Node中,事件主要来源于网络请求、文件I/O等,这些事件都有对应的网络I/O观察者、文件I/O观察者等,事件循环则从观察者那里取出事件并处理。

2.3 请求对象

从Javascript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,叫做请求对象。以最简单的Windows下fs.open()方法(根据指定路径和参数去打开一个文件并得到一个文件描述符)为例,从JS调用到内建模块通过libuv进行系统调用,实际上是调用了uv_fs_open()方法。在调用过程中,创建了一个FSReqWrap请求对象,从JS层传入的参数和方法都封装在这个请求对象中,其中我们最为关注的回调函数被设置在这个对象的oncompete_sym属性上。对象包装完毕后,将FSReqWrap对象推入线程池中等待执行。

至此,JS调用立即返回,JS线程可以继续执行后续操作。当前的I/O操作在线程池中等待执行,这就完成了异步调用的第一阶段。

2.4 执行回调

回调通知是异步I/O的第二阶段。线程池中的I/O操作调用完毕后,会将获取的结果储存起来,然后通知IOCP当前对象操作已完成,并将线程归还线程池。在每次Tick的执行中,事件循环的I/O观察者会调用相关的方法检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到I/O观察者的队列中,然后将其当做事件处理。

Node.js异步I/O学习笔记

3. 非I/O的异步API

Node中还存在一些与I/O无关的异步API,例如定时器setTimeout()、setInterval(),立即异步执行任务的process.nextTick()和setImmdiate()等,这里略微介绍一下。

3.1 定时器API

setTimeout()和setInterval()浏览器端的API是一致的,它们的实现原理与异步I/O类似,只是不需要I/O线程池的参与。调用定时器API创建的定时器会被插入到定时器观察者内部的一棵红黑树中,每次事件循环的Tick都会从红黑树中迭代取出定时器对象,检查是否超过定时时间,若超过就形成一个事件,回调函数立即被执行。定时器的主要问题在于它的定时时间并非特别精确(毫秒级,在容忍范围内)。

3.2 立即异步执行任务API

在Node出现之前,很多人也许为了立即异步执行一个任务,会这样调用:

setTimeout(function() {

    // TODO

}, 0);

由于事件循环的特点,定时器的精确度不够,而且采用定时器需要使用红黑树,各种操作时间复杂度为O(log(n))。而process.nextTick()方法只会将回调函数放入队列中,在下一轮Tick时取出执行,复杂度为O(1)更为高效。

此外还有一个setImmediate()方法和上述方法类似,都是将回调函数延迟执行。不过前者的优先级要比后者高,这是因为事件循环对观察者的检查是有先后顺序的。另外,前者的回调函数保存在一个数组中,每轮Tick会将数组中的所有回调函数全部执行完;后者结果保存在链表中,每轮Tick只会执行一个回调函数。

4. 事件驱动与高性能服务器

前面以fs.open()为例阐述了Node如何实现异步I/O。事实上对网络套接字的处理,Node也应用了异步I/O,这也是Node构建Web服务器的基础。经典的服务器模型有:

1.同步式:一次只能处理一个请求,其余请求都处于等待状态
2.每进程/每请求:为每个请求启动一个进程,但系统资源有限,不具备扩展性
3.每线程/每请求:为每个请求启动一个线程。线程比进程要轻量,但每个线程都占用一定内存,当大并发请求到来时,内存很快就会用光

著名的Apache采用的就是每线程/每请求的形式,这也是它难以应对高并发的原因。Node通过事件驱动方式处理请求,可以省掉创建和销毁线程的开销,同时操作系统在调度任务时因为线程较少,上下文切换的代价也很低。即使在大量连接的情况下,Node也能有条不紊地处理请求。

知名服务器Nginx也摒弃了多线程的方式,采用和Node一样的事件驱动方式。如今Nginx大有取代Apache之势。Nginx采用纯C编写,性能较高,但是它仅适合做Web服务器,用于反向代理或负载均衡等。Node可以构建与Nginx相同的功能,也可以处理各种具体业务,自身性能也不错。在实际项目中,我们可以结合它们各自有点,以达到应用的最佳性能。

Javascript 相关文章推荐
popdiv
Jul 14 Javascript
IE 条件注释详解总结(附实例代码)
Aug 29 Javascript
用js来刷新当前页面保留参数的具体实现
Dec 23 Javascript
jQuery 回车事件enter使用示例
Feb 18 Javascript
javascript实现table表格隔行变色的方法
May 13 Javascript
js实现左侧网页tab滑动门效果代码
Sep 06 Javascript
JavaScript中boolean类型之三种情景实例代码
Nov 21 Javascript
JavaScript中最常见的三个面试题解析
Mar 04 Javascript
前端把html表格生成为excel表格的实例
Sep 19 Javascript
jQuery插件实现的日历功能示例【附源码下载】
Sep 07 jQuery
vue实现的组件兄弟间通信功能示例
Dec 04 Javascript
node后端服务保活的实现
Nov 10 Javascript
JavaScript中的ubound函数使用实例
Nov 04 #Javascript
JavaScript实现检查页面上的广告是否被AdBlock屏蔽了的方法
Nov 03 #Javascript
网页中表单按回车就自动提交的问题的解决方案
Nov 03 #Javascript
详解jquery中$.ajax方法提交表单
Nov 03 #Javascript
jquery处理json对象
Nov 03 #Javascript
js格式化时间小结
Nov 03 #Javascript
解决js下referer兼容各大浏览器的方法
Nov 03 #Javascript
You might like
PHP SplObjectStorage使用实例
2015/05/12 PHP
PHP isset empty函数相关面试题及解析
2020/12/11 PHP
让textarea自动调整大小的js代码
2011/04/12 Javascript
分享10篇优秀的jQuery幻灯片制作教程及应用案例
2011/04/16 Javascript
Jquery submit()无法提交问题
2013/04/21 Javascript
使用jquery自定义鼠标样式满足个性需求
2013/11/05 Javascript
javascript自动给文本url地址增加链接的方法分享
2014/01/20 Javascript
JQuery查找DOM节点的方法
2015/06/11 Javascript
javascript实现10个球随机运动、碰撞实例详解
2015/07/08 Javascript
jquery自适应布局的简单实例
2016/05/28 Javascript
前端js实现文件的断点续传 后端PHP文件接收
2016/10/14 Javascript
JavaScript数组操作详解
2017/02/04 Javascript
微信小程序之页面拦截器的示例代码
2017/09/07 Javascript
微信小程序日期时间选择器使用方法
2018/02/01 Javascript
在vscode中统一vue编码风格的方法
2018/02/22 Javascript
JavaScript学习笔记之DOM操作实例分析
2019/01/08 Javascript
JS实现音量控制拖动
2020/01/15 Javascript
Vue+Element UI 树形控件整合下拉功能菜单(tree + dropdown +input)
2020/08/28 Javascript
python遍历类中所有成员的方法
2015/03/18 Python
Python中遇到的小问题及解决方法汇总
2017/01/11 Python
Python实现的基数排序算法原理与用法实例分析
2017/11/23 Python
flask框架视图函数用法示例
2018/07/19 Python
tensorflow saver 保存和恢复指定 tensor的实例讲解
2018/07/26 Python
使用pygame写一个古诗词填空通关游戏
2019/12/03 Python
PyCharm Ctrl+Shift+F 失灵的简单有效解决操作
2021/01/15 Python
CSS改变网页中鼠标选中文字背景颜色例子
2014/04/23 HTML / CSS
法拉利英国精品店:Ferraris Boutique UK
2019/07/20 全球购物
大专自我鉴定范文
2013/10/01 职场文书
致1500米运动员广播稿
2014/02/07 职场文书
资源工程专业毕业生求职信
2014/02/27 职场文书
初中三好学生自我鉴定
2014/04/07 职场文书
2015年宣传部部长竞选演讲稿
2014/11/28 职场文书
大国崛起日本观后感
2015/06/02 职场文书
亮剑精神观后感
2015/06/05 职场文书
周一问候语大全
2015/11/10 职场文书
如何使用PyCharm及常用配置详解
2021/06/03 Python