Node.js异步IO原理剖析
为什么要异步I/O?
- 从用户体验角度讲,异步IO可以消除UI阻塞,快速响应资源
- JavaScript是单线程的,它与UI渲染共用一个线程。所以在JavaScript执行的时候,UI渲染将处于停顿的状态,用户体验较差。而异步请求可以在下载资源的时候,JavaScript和UI渲染都同时执行,消除UI阻塞,降低响应资源需要的时间开销。
- 假如一个资源来自两个不同位置的数据的返回,第一个资源需要M毫秒的耗时,第二个资源需要N毫秒的耗时。当采用同步的方式,总耗时为(M+N)毫秒,代码大致如下:
//耗时为M毫秒
getData('from_db');
//耗时为N毫秒
getData('from_remote_api');当采用异步的方式,总耗时为max(M,N),代码大致如下:
getData('from_db',function(result){
//消费时间为M
}); getData('from_remote_api',function(result){
//消费时间为N
});随着应用的复杂性,情景会变成M+N+...和max(M,N,...),此时同步和异步的优劣就会更加凸显。另一方面,随着网站和应用的扩展,数据往往会分布到多台服务器上,而分布意味着M和N的值会线性增长,这也会放大异步和同步在性能上的差异。总之,IO是昂贵的,分布式IO是更昂贵的!
- 从资源分配角度讲,异步IO可以让单线程远离阻塞,以更好地利用CPU
- 假设业务线上有一组互不相关的任务需要完成,现行的主流方法有以下两种:
- 单线程同步执行:会阻塞IO导致硬件资源和CPU得不到更优的使用
- 多线程并发执行:会出现死锁、状态同步等问题
- Node的解决方案
- 利用单线程,远离多线程的死锁、状态同步等问题;
- 利用异步I/O,让单线程远离阻塞,更好的利CPU
- 假设业务线上有一组互不相关的任务需要完成,现行的主流方法有以下两种:
异步IO实现现状?
- I/O的阻塞与非阻塞:IO对于操作系统内核而言,只有阻塞与非阻塞两种方式。阻塞模式的I/O会造成应用程序等待,直到I/O完成,会造成CPU等待IO,浪费等待时间,CPU的处理能力不能充分利用。同时操作系统也支持将I/O操作设置为非阻塞模式,这时应用程序的调用将可能在没有拿到真正数据时就立即返回了,为此应用程序需要多次调用才能确认I/O操作完全完成,但由于IO并没有完成,立即返回的并不是业务层期望的数据,仅仅是当前调用的状态。
- I/O的同步与异步:I/O的同步与异步出现在应用程序中。如果做阻塞I/O调用,应用程序等待调用的完成的过程就是一种同步状况。相反,I/O为非阻塞模式时,应用程序则是异步的。

图2-阻塞IO调用示意图 图3-非阻塞IO调用示意图
为了获取完整的数据,应用程序需要重复调用IO操作来确认是否完成,叫做轮询。轮询技术主要有以下这些:
- read:它是最原始,性能最低的一种。
- select:在read基础上的改进方案,通过对文件描述符上的事件状态进行判断。
- poll:较select有所改进,采用链表的方式避免数据长度的限制,其次它能避免不必要的检查。但在文件描述符过多时,性能十分低下。
- epoll:该方案是Linux下效率最高的IO事件通知机制,具体方法见图。

图4-read方式 图5-select方式

图6-poll方式 图7-epoll方式
尽管epoll已经利用事件来降低了CPU的耗用,但是休眠期间CPU是闲置的,对于当前线程而言利用率不够。那么,是否有一种理想的异步I/O呢?
答案是当然有!理想的异步I/O实现如下图所示:

图8-理想中的异步IO方式示意图
天真的程序员们曾经幻想:我们可以通过信号或回调将数据传递给应用程序啊!
的确,Linux下提供了一种异步IO的方式(AIO),它就是通过信号或回调传递数据的。
但不幸的是,只有Linux下有,而且还存在一些如系统缓存无法利用的缺陷。
后来,经过反复的思考与实践,现实中的异步IO是这样实现的:

图9-现实中的异步IO示意图
其核心思想是:利用多线程,主线程负责计算,IO线程负责IO操作,线程间通过信号进行通信。
聪明的你也许会问:Node不是单线程吗,如何实现多线程呢?
其实,Node底层是可以实现多线程的,只是上层提供给用户的JavaScript是单线程的。而这里,我们探讨的正是Node底层是如何实现异步IO的?
如果再细一点,其实Linux是利用如上的多线程创建线程池实现的,而Windows则是利用IOCP创建的。

图10-Node异步IO底层实现框架图
Node异步IO的实现?
在清楚了Node底层对异步IO的实现原理后,我们就可以进一步理解一个Node进程是如何实现完整的异步IO的了!
- Node的异步I/O模型:事件循环、观察者、请求对象、执行回调是四个核心概念。
- 事件循环:进程启动时,Node会创建一个类似while(true)的循环,判断是否有事件需要处理,若有,取出事件并执行回调函数。

- 观察者:观察者是用来判断是否有事件需要处理。事件循环中有一到多个观察者,判断过程会向观察者询问是否有需要处理的事件。这个过程类似于饭店的厨师与前台服务员的关系。厨师每做完一轮菜,就会向前台服务员询问是否有要做的菜,如果有就继续做,没有的话就下班了。这一过程中,前台服务员就相当于观察者,她收到的顾客点单就是回调函数。
注:事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求是生产者,而事件循环则从观察者那里取出事件并处理。
- 请求对象:实际上,从JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,叫做请求对象。以fs.open( )方法为例,
fs.open = function(path, flags, mode, callback) { //... binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, callback); };这个函数的作用是根据指定的路径和参数去打开一个文件,从而得到一个文件描述符,是后续所有I/O操作的初始操作。
- 请求对象:实际上,从JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,叫做请求对象。以fs.open( )方法为例,
整个调用过程:JavaScript -> Node核心模块 -> C++内建模块 -> libuv系统调用
在uv_fs_open的调用过程中,Node.js创建了一个FSReqWrap请求对象。从JavaScript传入的参数和当前方法都被封装在这个请求对象中,其中回调函数则被设置在这个对象的oncomplete_sym属性上。
req_wrap->object_->Set(oncomplete_sym, callback);
对象包装完毕后,调用QueueUserWorkItem方法将这个FSReqWrap对象推入线程池中等待执行。
QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTELONGFUNCTION)
QueueUserWorkItem接受三个参数,第一个是要执行的方法,第二个是方法的上下文,第三个是执行的标志。
至此,由JavaScript层面发起的异步调用第一阶段就此结束。
- 执行回调:组装好请求对象,送入I/O线程池等待执行,实际上完成了异步I/O的第一部分,回调通知是第二部分。当线程池中有可用线程的时候调用uv_fs_thread_proc方法执行。该方法会根据传入的类型调用相应的底层函数,以uv_fs_open为例,实际会调用到fs__open方法。调用完毕之后,会将获取的结果设置在req->result上。然后调用PostQueuedCompletionStatus通知我们的IOCP*对象操作已经完成,并将线程归还给线程池。
PostQueuedCompletionStatus((loop)->iocp, 0, 0, &((req)->overlapped)
PostQueuedCompletionStatus方法的作用是向创建的IOCP上相关的线程通信,线程根据执行状况和传入的参数判定退出。
在这一过程中,每次事件循环会调用GetQueuedCompletionStatus()方法检查线程池中是否有执行完的请求,若有,会将请求对象加入到I/O观察者的队列中,将其作为事件处理。
I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,取出oncomplete_sym属性作为方法,然后调用执行,以此达到执行回调函数的目的。

图13-Node整个异步IO流程示意图
总结
- JavaScript是单线程的,但Node本身其实是多线程的,除了用户代码无法并行执行外,所有的I/O请求是可以并行执行的。
- 事件循环是Node异步I/O实现的核心,Node通过事件驱动的方式处理请求,使得其无须为每个请求创建额外的线程,省掉了创建和销毁线程的开销。同时也因为线程数较少,不受线程上下文切换的影响,维持了Node的高性能。
- Node异步IO、非阻塞的特性,使它非常适用于IO密集、高并发的应用场景。
Node.js异步IO原理剖析的更多相关文章
- 深入理解node.js异步编程:基础篇
###[本文是基础内容,大神请绕道,才疏学浅,难免纰漏,请各位轻喷] ##1. 概述 目前开源社区最火热的技术当属Node.js莫属了,作为使用Javascript为主要开发语言的服务器端编程技术和平 ...
- 转载:node.js socket.io
本文转自:http://www.xiaocai.name/post/cf1f9_7b6507 学习node.js socket.io 使用 用node.js(socket.io)实现数据实时推送 在 ...
- Node.js异步处理CPU密集型任务
Node.js异步处理CPU密集型任务 Node.js擅长数据密集型实时(data-intensive real-time)交互的应用场景.然而数据密集型实时应用程序并非仅仅有I/O密集型任务,当碰到 ...
- Node.js机制及原理理解初步【转】
一.node.js优缺点 node.js是单线程. 好处就是 1)简单 2)高性能,避免了频繁的线程切换开销 3)占用资源小,因为是单线程,在大负荷情况下,对内存占用仍然很低 3)线程安全,没有加锁. ...
- Node.js机制及原理理解初步
http://blog.csdn.net/leftfist/article/details/41891407 一.node.js优缺点 node.js是单线程. 好处就是 1)简单 2)高性能,避免了 ...
- 使用Node.js+Socket.IO搭建WebSocket实时应用
Web领域的实时推送技术,也被称作Realtime技术.这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新.它有着广泛的应用场景,比如在线聊天室.在线客服系统.评论系统.WebIM等. W ...
- (转)使用Node.js+Socket.IO搭建WebSocket实时应用
Web领域的实时推送技术,也被称作Realtime技术.这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新.它有着广泛的应用场景,比如在线聊天室.在线客服系统.评论系统.WebIM等. W ...
- 使用Node.js+Socket.IO搭建WebSocket实时应用【转载】
原文:http://www.jianshu.com/p/d9b1273a93fd Web领域的实时推送技术,也被称作Realtime技术.这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新 ...
- 深入研究Node.js的底层原理和高级使用
深入研究Node.js的底层原理和高级使用
随机推荐
- Linux系统中到底应该怎么理解系统的平均负载
02 | 基础篇:到底应该怎么理解“平均负载”? 每次发现系统变慢时,我们通常做的第一件事,就是执行 top 或者 uptime 命令,来了解系统的负载情况.比如像下面这样,我在命令行里输入了 upt ...
- scrapy 基础组件专题(十四):scrapy CookiesMiddleware源码
一 Scrapy框架--cookie的获取/传递/本地保存 1. 完成模拟登陆2. 登陆成功后提取出cookie,然后保存到本地cookie.txt文件中3. 再次使用时从本地的cookie.txt中 ...
- python 生成器(二):生成器基础(二)惰性实现
简介 设计 Iterator 接口时考虑到了惰性:next(my_iterator) 一次生成一个元素.懒惰的反义词是急迫,其实,惰性求值(lazy evaluation)和及早求值(eager ev ...
- Python函数03/函数名的第一类对象及使用/f 格式化/迭代器/递归
Python函数03/函数名的第一类对象及使用/f 格式化/迭代器/递归 目录 Python函数03/函数名的第一类对象及使用/f 格式化/迭代器/递归 内容纲要 1.函数名的第一类对象及使用 2.f ...
- mysql子查询习题98
#1.查询工资最低的员工信息:last name, salary SELECT last_name, salary FROM employees WHERE salary = ( SELECT MIN ...
- 重学c#系列——c#运行原理(二)
前言 c# 是怎么运行的呢?是否和java一样运行在像jvm的虚拟机上呢?其实差不多,但是更广泛. c# 运行环境不仅c#可以运行,符合.net framework 开发规范的都可以运行. c# 程序 ...
- 机器学习实战---决策树CART简介及分类树实现
https://blog.csdn.net/weixin_43383558/article/details/84303339?utm_medium=distribute.pc_relevant_t0. ...
- react实战 : react 与 svg
有一个需求是这样的. 一个组件里若干个区块.区块数量不定. 区块里面是一个波浪效果组件,而这个一般用 SVG 做. 所以就变成了在 react 中使用 SVG 的问题. 首先是波浪效果需要的样式. . ...
- 题解 洛谷 P6142 【[USACO20FEB]Delegation P】
和赛道修建类似,先对\(k\)进行二分,将最值问题转化为判定问题. 在判定一个\(k\)是否合法时,贪心去考虑,一个节点下面的若干条链在合并时,一条链肯定和另一条使它合并后恰好满足长度限制的链合并最优 ...
- Echarts柱状图顶部加数量显示
//加在series中itemStyle: { normal: { label: { show: true, position: 'top', textStyle: { color: '#615a5a ...