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 相关文章推荐
Jqyery中同等与js中windows.onload的应用
May 10 Javascript
js定时器怎么写?就是在特定时间执行某段程序
Oct 11 Javascript
JS动态调用方法名示例介绍
Dec 18 Javascript
js在指定位置增加节点函数insertBefore()用法实例
Jan 12 Javascript
javascript数组输出的两种方式
Jan 13 Javascript
使用jquery清空、复位整个输入域
Apr 02 Javascript
JS实现图片高斯模糊切换效果的焦点图实例
Jan 21 Javascript
jQuery实现文件编码成base64并通过AJAX上传的方法
Apr 12 jQuery
使用Vue自定义指令实现Select组件
May 24 Javascript
Vue2.x通用编辑组件的封装及应用详解
May 28 Javascript
解决Vue router-link绑定事件不生效的问题
Jul 22 Javascript
什么是SOLID
Mar 24 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
模仿OSO的论坛(一)
2006/10/09 PHP
使用PHP实现二分查找算法代码分享
2011/06/24 PHP
比file_get_contents稳定的curl_get_contents分享
2012/01/11 PHP
php目录遍历函数opendir用法实例
2014/11/20 PHP
Yii2.0中的COOKIE和SESSION用法
2016/08/12 PHP
php 输出json及显示json中的中文汉字详解及实例
2016/11/09 PHP
PHP写的简单数字验证码实例
2017/05/23 PHP
PHP赋值的内部是如何跑的详解
2019/01/13 PHP
jquery的Tooltip插件 qtip使用详细说明
2010/09/08 Javascript
jQuery简单图表peity.js使用示例
2014/05/02 Javascript
JS在IE下缺少标识符的错误
2014/07/23 Javascript
javascript实现在网页中运行本地程序的方法
2016/02/03 Javascript
JSON 对象未定义错误的解决方法
2016/09/29 Javascript
jQuery实现级联下拉框实战(5)
2017/02/08 Javascript
基于VUE.JS的移动端框架Mint UI的使用
2017/10/11 Javascript
多线程爬虫批量下载pcgame图片url 保存为xml的实现代码
2013/01/17 Python
python通过urllib2爬网页上种子下载示例
2014/02/24 Python
Python出现segfault错误解决方法
2016/04/16 Python
Python的Flask框架应用调用Redis队列数据的方法
2016/06/06 Python
python如何实现内容写在图片上
2018/03/23 Python
Python实现OpenCV的安装与使用示例
2018/03/30 Python
python中for循环输出列表索引与对应的值方法
2018/11/07 Python
Python-ElasticSearch搜索查询的讲解
2019/02/25 Python
Python使用itchat模块实现群聊转发,自动回复功能示例
2019/08/26 Python
python中Mako库实例用法
2020/12/31 Python
维氏瑞士军刀英国网站:Victorinox英国
2019/07/04 全球购物
工商管理专业实习大学生自我鉴定
2013/09/19 职场文书
大四学年自我鉴定
2013/11/13 职场文书
国际贸易个人求职信范文
2014/01/04 职场文书
大一军训感言
2014/01/09 职场文书
2014年度培训工作总结
2014/11/27 职场文书
入党函调证明材料
2014/12/24 职场文书
安全守法证明
2015/06/23 职场文书
2016年学校党支部公开承诺书
2016/03/25 职场文书
创业项目(超低成本创业项目)
2019/08/16 职场文书
windows11怎么查看自己安装的版本号? win11版本号的查看方法
2021/11/21 数码科技