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

“异步”这个名词的大规模流行是在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观察者的队列中,然后将其当做事件处理。

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相同的功能,也可以处理各种具体业务,自身性能也不错。在实际项目中,我们可以结合它们各自有点,以达到应用的最佳性能。

时间: 2024-12-23 19:03:13

Node.js异步I/O学习笔记_node.js的相关文章

Node.js中的模块机制学习笔记_node.js

Javascript自诞生以来,曾经没有人拿它当做一门编程语言.在Web 1.0时代,这种脚本语言主要被用来做表单验证和网页特效.直到Web 2.0时代,前端工程师利用它大大提升了网页上的用户体验,JS才被广泛重视起来.在JS逐渐流行的过程中,它大致经历了工具类库.组件库.前端框架.前端应用的变迁.Javascript先天就缺乏一项功能:模块,而CommonJS规范的出现则弥补了这一缺陷.本文将介绍CommonJS规范及Node的模块机制. 在其他高级语言中,Java有类文件,Python有im

Lua表达式和控制结构学习笔记_node.js

算术操作符 Lua 的算术操作符有: "+"(加法): 复制代码 代码如下: print(1 + 2) "-"(减法): 复制代码 代码如下: print(2 - 1) "*"(乘法): 复制代码 代码如下: print(1 * 2) "/"(除法): 复制代码 代码如下: print(1 / 2) "^"(指数): 复制代码 代码如下: print(27^(-1/3)) "%"(取模)

深入分析node.js的异步API和其局限性_node.js

用异步API的原因 异步的概念之所以首先在Web2.0中火起来,是因为在浏览器中Javascript在单线程上执行,而且他还与UI渲染公用一个线程.这意味着Javascript在执行的时候UI渲染和响应是处于停滞状态的.为了用户体验更好而采取异步的方式(当然,这在所谓的单线程语言中)不阻塞主线程继续响应用户操作.这属于用户体验的范畴. 同样的,如果有其他语言经验的工程师当然也明白,CPU在线程间切换是需要消耗大量的时间的(主要为上下文之间的切换和缓存),所以提高效率也是使用异步API的理由. 当

用C/C++来实现 Node.js 的模块(二)_node.js

温故而知新,可以为湿矣 首先请大家记住这个 V8 的在线手册--http://izs.me/v8-docs/main.html. 还记得上次的 building.gyp 文件吗? 复制代码 代码如下: {   "targets": [     {       "target_name": "addon",       "sources": [ "addon.cc" ]     }   ] }   就像这样,

深入理解Node.js 事件循环和回调函数_node.js

本文详细的介绍了Node.js 事件循环和Node.js回调函数,废话不多说了,具体看下面把.  一.Node.js 事件循环 Node.js 是单进程单线程应用程序,但是通过事件和回调支持并发,所以性能非常高.Node.js 的每一个 API 都是异步的,并作为一个独立线程运行,使用异步函数调用,并处理并发.Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现.Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观

浅析Node.js中的内存泄漏问题_node.js

 这篇文章是由Mozilla的Identity团队带来的 A Node.JS Holiday Season系列文章的首篇,该团队上个月发布了 Persona的第一个测试版本.在开发Persona时我们构建了一系列的工具,包括了从调试,到本地化,到依赖管理以及更多的方面.在这一系列的文章中我们将与社区分享我们的经验和这些工具,这对任何想用node.js建立一个高可用性服务的人都很有用.我们希望您能喜欢这些文章,并期待看到您的想法和贡献. 我们将从一篇关于Node.js的实质性问题:内存泄漏的主题文

总结Node.js中的一些错误类型_node.js

前言 Node.js应用中可能会发生多种类型的错误.如:当出现语法错误或运行时错误时,会触发JavaScript错误:当试图试访问一个不存在或没有访问的文件时,会触发系统错误:除JavaScript错误和系统错误错误外,用户还可以自定义错误. 1. 错误分类 Node.js是一个JavaScript运行时平台,其应用发生错误都是一个Error实例或Error子类的实例. 在Node.js应用中,可能发生的错误有以下4类:      1.标准JavaScript错误,如:           <E

node.js插件nodeclipse安装图文教程_node.js

在eclipse插件中,node.js插件中比较知名的是nodeclipse,本文为大家介绍了nodeclipse安装教程,供大家参考,具体内容如下 安装nodeclipse 1. 依次点击工具→插件安装→浏览eclipse插件市场→搜索node.js→在搜索结果里找nodeclipse如下图 2.点击上图中的install开始加载,加载完毕点下一步接受许可协议静待安装完成即可 3. 安装完毕后重启HBuilder 4. 新建node项目:依次点击菜单文件→新建→其他如下图 5.选择要新建的no

Node.js模块加载详解_node.js

JavaScript是世界上使用频率最高的编程语言之一,它是Web世界的通用语言,被所有浏览器所使用.JavaScript的诞生要追溯到Netscape那个时代,它的核心内容被仓促的开发出来,用以对抗Microsoft,参与当时白热化的浏览器大战.由于过早的发布,无可避免的造成了它的一些不太好的特性. 尽管它的开发时间很短,但是JavaScript依然具备了很多强大的特性,不过,每个脚本共享一个全局命名空间这个特性除外. 一旦Web页面加载了JavaScript代码,它就会被注入到全局命名空间,