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 相关文章推荐
Extjs中使用extend(js继承) 的代码
Mar 15 Javascript
使用JavaScript修改浏览器URL地址栏的实现代码
Oct 21 Javascript
超级简单实现JavaScript MVC 样式框架
Mar 24 Javascript
基于jQuery日历插件制作日历
Mar 11 Javascript
JS和jQuery使用submit方法无法提交表单的原因分析及解决办法
May 17 Javascript
详解JS几种变量交换方式以及性能分析对比
Nov 25 Javascript
详解webpack 入门总结和实践(按需异步加载,css单独打包,生成多个入口文件)
Jun 20 Javascript
JavaScript实现跟随滚动缓冲运动广告框
Jul 15 Javascript
javascript算法之二叉搜索树的示例代码
Sep 12 Javascript
详解bootstrap导航栏.nav与.navbar区别
Nov 23 Javascript
在axios中使用params传参的时候传入数组的方法
Sep 25 Javascript
javascript自定义右键菜单插件
Dec 16 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 INI配置文件的解析实现分析
2011/01/04 PHP
Symfony页面的基本创建实例详解
2015/01/26 PHP
php中实现获取随机数组列表的自定义函数
2015/04/02 PHP
Nigma vs Liquid BO3 第二场2.13
2021/03/10 DOTA
jQuery中文入门指南,翻译加实例,jQuery的起点教程
2007/01/13 Javascript
javascript下有关dom以及xml节点访问兼容问题
2007/11/26 Javascript
使用jquery实现div的tab切换实例代码
2013/05/27 Javascript
window.onerror()的用法与实例分析
2016/01/27 Javascript
js简单判断移动端系统的方法
2016/02/25 Javascript
js实现表单及时验证功能 用户信息立即验证
2016/09/13 Javascript
详解nodejs微信公众号开发——5.素材管理接口
2017/04/11 NodeJs
jQuery接受后台传递的List的实例详解
2017/08/02 jQuery
基于zepto.js实现登录界面
2017/10/09 Javascript
使用vux实现上拉刷新功能遇到的坑
2018/02/08 Javascript
webpack4之SplitChunksPlugin使用指南
2018/06/12 Javascript
使用localStorage替代cookie做本地存储
2019/09/25 Javascript
vue实现配置全局访问路径头(axios)
2019/11/01 Javascript
JS正则表达式验证密码强度
2020/03/18 Javascript
vue中使用router全局守卫实现页面拦截的示例
2020/10/23 Javascript
python实现机器学习之多元线性回归
2018/09/06 Python
浅谈Python_Openpyxl使用(最全总结)
2019/09/05 Python
python3实现弹弹球小游戏
2019/11/25 Python
如何基于Python实现数字类型转换
2020/02/07 Python
Python使用Selenium模拟浏览器自动操作功能
2020/09/08 Python
HTML5之SVG 2D入门9—蒙板及mask元素介绍与应用
2013/01/30 HTML / CSS
林清轩官方网站:山茶花润肤油开创者
2016/10/26 全球购物
英国音乐设备和乐器商店:Gear4music
2017/10/16 全球购物
Dr.Jart+美国官网:韩国药妆品牌
2019/01/18 全球购物
四年的个人工作自我评价
2013/12/10 职场文书
法人代表证明书
2014/09/18 职场文书
小学校园广播稿
2015/08/18 职场文书
2016年先进教师个人事迹材料
2016/02/26 职场文书
导游词之无锡东林书院
2019/12/11 职场文书
MySQL 十大常用字符串函数详解
2021/06/30 MySQL
Java基础之this关键字的使用
2021/06/30 Java/Android
Arthas排查Kubernetes中应用频繁挂掉重启异常
2022/02/28 MySQL