前言:Nodejs最赖以自豪的优势莫过于“单线程实现异步IO”了,也许你仍然丈二和尚摸不着头脑,Nodejs自我标榜是单线程,还能实现异步IO操作,这两者难道不是相互矛盾的么?葫芦里到底藏着什么药? 且听我娓娓道来……


一、首先,看看Nodejs的架构

http://nodejs.cn/download/ 你可以到Nodejs中文网下载Node源码。

Nodejs结构大体分为三个部分:

1)Node.js标准库:这部分由JavaScript编写。也就是平时我们经常require的各个模块,如:http,fs、express,request…… 这部分在源码的lib目录下可以看到;

2)Node bingdings: nodejs程序的main函数入口,还有提供给lib模块的C++类接口,这一层是javascript与底层C/C++沟通的桥梁,由C++编写,这部分在源码的src目录下可以看到;

3)最底层,支持Nodejs运行的关键: V8 引擎:用来解析、执行javascript代码的运行环境。 libuv: 提供最底层的IO操作接口,包括文件异步IO的线程池管理和网络的IO操作,是整个异步IO实现的核心! 这部分由C/C++编写,在源码的deps目录下可以看到。

小结:我们其实对 Node.js的单线程一直有个很深的误会。事实上,这里的“单线程”指的是我们(开发者)编写的代码只能运行在一个线程当中(习惯称之为主线程),Node.js并没有给 Javascript 执行时创建新线程的能力,所以称为单线程,也就是所谓的主线程。 其实,Nodejs中许多异步方法在具体的实现时(NodeJs底层封装了Libuv,它提供了线程池、事件池、异步I/O等模块功能,其完成了异步方法的具体实现),内部均采用了多线程机制。

二、异步IO操作调用流程

这里,主线程就是nodejs所谓的单线程,也就是用户javascript代码运行的线程

IO线程是由Libuv(Linux下由libeio具体实现;window下则由IOCP具体实现)管理的线程池控制的,本质上是多线程。即采用了线程池与阻塞IO模拟了异步IO。

以文件操作为例子,回调函数是何时被加载执行的呢?也就是异步IO操作内部是如何实现的?

新建一个文件yzx_file.js ,内容如下:

var fs = require('fs');
var path = require('path'); fs.readFile(__dirname + '/test01.txt', {flag: 'r+', encoding: 'utf8'}, function (err, data) { console.log(data); //打印test01.txt文本内容
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

整个文件操作的调用过程如下:

1)首先,用户写的javascript调用Node的核心模块fs.js ;

2)接下来,Node的核心模块调用C++内建模块node_file.cc ;

3)最后,根据不同平台(Linux或者window),内建模块通过libuv进行系统调用

然后,接下来你可能会产生疑问:那回调函数何时被执行呢?

三、Nodejs运行流程

当你运行上面的例子,如 node yzx_file.js,剖析内部的具体流程。

1)node启动,进入main函数;

2)初始化核心数据结构 default_loop_struct;这个数据结构是事件循环的核心,当node执行到“加载js文件”时,如果用户的javascript代码中具有异步IO操作时,如读写文件。这时候,javascript代码调用–>lib模块–>C++模块–>libuv接口–>最终系统底层的API—>系统返回一个文件描述符fd 和javascript代码传进来的回调函数callback,然后封装成一个IO观察者(一个uv__io_s类型的对象),保存到default_loop_struct。

(文件描述符的理解: 对于每个程序系统都有一张单独的表。精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。)

(观察者的理解:在每个Tick(在程序启动时,Node便会创建一个类似于while(true)的循环,没执行一次循环体的过程我们称为Tick)的过程中,为了判断是否有事件需要处理,所以引入了观察者的概念,每个事件循环中有一个或多个观察者,判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。在node中,事件主要来源于网络请求,文件IO等,这些事件对应的观察者有文件I/O观察者、网络I/O观察者等。事件轮询是一个典型的生产者、消费者模型,异步I/O、网络请求等则是事件的生产者,源源不断为node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。)

3)加载用户javascript文件,调用V8引擎接口,解析并执行javascript代码; 如果有异步IO,则通过一系列调用系统底层API,若是网络IO,如http.get() 或者 app.listen() ;则把系统调用后返回的结果(文件描述符fd)和事件绑定的回调函数callback,一起封装成一个IO观察者,保存到default_loop_struct;如果是文件IO,例如在uv_fs_open()的调用过程中,我们创建了一个FSReqWrap请求对象。从JavaScript层传入的参数和当前方法都被封装在这个请求对象中,其中我们最为关心的回调函数则被设置在这个对象的oncomplete_sym属性上:req_wrap->object_->Set(oncomplete_sym, callback);对象包装完毕后,在Windows下,则调用QueueUserWorkItem()方法将这个FSReqWrap对象推入线程池中等待执行,该方法的代码如下所示QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTEDEFAULT);QueueUserWorkItem()方法接收3个参数:第一个参数是将要执行的方法的引用,这里引用的是uv_fs_thread_proc,这个参数是uv_fs_thread_proc运行时所需要的参数;第三个参数是执行的标志。当线程池中有可用线程时,我们会调用uv_fs_thread_proc()方法。uv_fs_thread_proc()方法会根据传入参数的类型调用相应的底层函数。以uv_fs_open()为例,实际上调用的是fs__open()方法。

至此,JavaScript调用立即返回,由JavaScript层面发起的异步调用的第一阶段就此结束。JavaScript线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不管它是否会阻塞I/O,都不会影响到JavaScript线程的后续执行,如此就达到到了异步的目的。

4)进入事件循环,即调用libuv的事件循环入口函数uv_run();当处理完 js代码,如果有io操作,那么这时default_loop_struct是保存着对应的io观察者的。处理完js代码,main函数继续往下调用libuv的事件循环入口uv_run(),node进程进入事件循环:

uv_run()的while循环做的就是一件事,判断default_loop_struct是否有存活的io观察者。 
    a. 如果没有io观察者,那么uv_run()退出,node进程退出。 
    b. 而如果有io观察者,那么uv_run()进入epoll_wait(),线程挂起等待,监听对应的io观察者是否有数据到来。有数据到来调用io观察者里保存着的callback(js代码),没有数据到来时一直在epoll_wait()进行等待。

5)这里要强调的是:只有用户的js代码全部执行完后,nodejs才调用libuv的事件循环入口函数uv_run(),即回调函数才有可能被执行。所以,如果主线程的js代码调用了阻塞方法,那么整个事件轮询就会被阻塞,事件队列中的事件便得不到及时处理。 为了验证这个事实:我做了一个实验如下:

新建 index.js文件,内容如下:(同时在根目录下新建一个test01.tet文件,内容为“我是test01!”)

var fs = require('fs');
var path = require('path'); fs.readFile(__dirname + '/test01.txt', {flag: 'r+', encoding: 'utf8'}, function (err, data) { console.log(data); //打印test01.txt文本内
}); //自己写的一个延迟函数
function sleep(milliSeconds){
var StartTime =new Date().getTime();
while (new Date().getTime() <StartTime+milliSeconds);
} sleep(5000); //延迟5s

程序很简单,即在主线程中,调用了一个阻塞函数,延时5s;运行程序,你会发现, 
5s以后,异步文件操作的回调函数才会被触发执行。这也说明了,如果真正想做到异步IO操作,主线程应该尽量避免大量的耗时计算或调用阻塞函数

总结:事件循环、观察者、请求对象、IO线程池这四者共同构成了Node异步IO操作的基本要素。

深入剖析Nodejs的异步IO的更多相关文章

  1. 深入理解nodejs的异步IO与事件模块机制

    node为什么要使用异步I/O 异步I/O的技术方案:轮询技术 node的异步I/O nodejs事件环 一.node为什么要使用异步I/O 异步最先诞生于操作系统的底层,在底层系统中,异步通过信号量 ...

  2. NodeJS示例异步式(Asynchronous)IO与同步式Synchronous)IO

    理解IO      IO(Input/Output)通常是指计算机线程进行慈磁盘读写或者网络通信时的一种行为.   同步式(Synchronous)IO和异步式(Asynchronous )IO   ...

  3. SQLite剖析之异步IO模式、共享缓存模式和解锁通知

    1.异步I/O模式    通常,当SQLite写一个数据库文件时,会等待,直到写操作完成,然后控制返回到调用程序.相比于CPU操作,写文件系统是非常耗时的,这是一个性能瓶颈.异步I/O后端是SQLit ...

  4. Node.js异步IO原理剖析

    为什么要异步I/O? 从用户体验角度讲,异步IO可以消除UI阻塞,快速响应资源 JavaScript是单线程的,它与UI渲染共用一个线程.所以在JavaScript执行的时候,UI渲染将处于停顿的状态 ...

  5. [.NET] 利用 async & await 进行异步 IO 操作

    利用 async & await 进行异步 IO 操作 [博主]反骨仔 [出处]http://www.cnblogs.com/liqingwen/p/6082673.html  序 上次,博主 ...

  6. 【译】深入理解python3.4中Asyncio库与Node.js的异步IO机制

    转载自http://xidui.github.io/2015/10/29/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3python3-4-Asyncio%E5%BA%93% ...

  7. 深入浅出ghostbuster剖析NodeJS与PhantomJS的通讯机制

    深入浅出ghostbuster剖析NodeJS与PhantomJS的通讯机制 蔡建良 2013-11-14 一. 让我们开始吧 通过命令行来执行 1) 进行命令窗口: cmd 2) 进入resourc ...

  8. nodejs中异步

    nodejs中的异步 1 nodejs 中的异步存在吗? 现在有点 javascript 基础的人都在听说过 nodejs ,而只要与 javascript 打交到人都会用或者是将要使用 nodejs ...

  9. nodejs之socket.io 聊天实现

    写在前面:最近很火的“996”话题,可谓是引起一片热议,马老师说:能够996应该是幸运的,996是对奋斗者的一种机遇(记得不是很清楚).996缺少的是自己的空闲时间了,当我是空闲的时候偶尔996挺好的 ...

随机推荐

  1. 关于TS流的解析

    字节.在TS流里可以填入很多类型的数据,如视频.音频.自定义信息等.他的包的结构为,包头为4个字节,负载为184个字节(这184个字节不一定都是有效数据,有一些可能为填充数据). 工作形式: 因为在T ...

  2. net.at.json.JSONException

    1.错误描述 严重:Servlet.service() for servlet [clientServlet] in context with path [/User] threw exception ...

  3. Android的sdk、api及工程目录说明

    SDK下包的说明1:add-ons:Android开发需要的第三方文件和软件库2:docs:Android的文档.包括开发指南.API参考.资源等3:extras:扩展的附加包4:platforms: ...

  4. C#图解教程 第二十四章 反射和特性

    反射和特性 元数据和反射Type 类获取Type对象什么是特性应用特性预定义的保留的特性 Obsolete(废弃)特性Conditional特性调用者信息特性DebuggerStepThrough 特 ...

  5. Centos7.2 搭建Lamp服务器以及迁移WordPress个人博客详细过程

    其实自己的博客搭了有段时间了,但是由于自己不太确定是不是一定要用wd的框架,以及实验室公网服务器的不稳定,就一直荒废着. 今天偶然间看到了腾讯云对于学生的优惠活动,毕业之前每月只要8元的云服务器(就算 ...

  6. MyISAM和InnoDB索引实现区别

    首先来讲MyISAM: MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址.下图是MyISAM索引的原理图: 这里设表一共有三列,假设我们以Col1为主键,则上图是 ...

  7. Python机器学习 (Python Machine Learning 中文版 PDF)

    Python机器学习介绍(Python Machine Learning 中文版) 机器学习,如今最令人振奋的计算机领域之一.看看那些大公司,Google.Facebook.Apple.Amazon早 ...

  8. [NOIP2017] 逛公园

    [NOIP2017] 逛公园 题目大意: 给定一张图,询问长度 不超过1到n的最短路长度加k 的1到n的路径 有多少条. 数据范围: 点数\(n \le 10^5\) ,边数\(m \le 2*10^ ...

  9. 期望$DP$ 方法总结

    期望\(DP\) 方法总结 这个题目太大了,变化也层出不穷,这里只是我的一点心得,不定期更新! 1. 递推式问题 对于无穷进行的操作期望步数问题,一般可用递推式解决. 对于一个问题\(ans[x]\) ...

  10. [BZOJ1543] 生成树计数 (Kruskal)

    Description 给定一个连通的带边权的图(允许自环和重边),求不同的最小生成树个数.两个生成树不同当它们所用的边的序号不同,换句话说,重边算多次. Input 第一行n,m,表示点数和边数(1 ...