前言: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. Netty(二):Netty为啥去掉支持AIO?

    匠心零度 转载请注明原创出处,谢谢! 疑惑 我们都知道bio nio 以及nio2(也就是aio),如果不是特别熟悉可以看看我之前写的网络 I/O模型,那么netty为什么还经常看到类似下面的这段代码 ...

  2. js 滚动到一定位置导航定位在页面最顶部

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. 利用js实现placeholder占位符,甩开ie不兼容

    正常的写法 <input type="text" placeholder="占位符"> 这种写法ie低版本的支持不友好,为了满足某些测试或者产品的变 ...

  4. Linux DHCP原理

    DHCP作用 DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)是一个局域网的网络协议,使用UDP协议工作, 主要有两个用途:给内部网络或网络服务供应 ...

  5. VC6安装错误——Error Launching acmboot.exe

    因项目需要,我需要安装Microsoft Visual C++ Professional Version 6 SP5.但是在安装时运行安装目录下的setup.exe,出现Error Launching ...

  6. Hybrid APP 架构设计思路

    关于Hybrid模式开发app的好处,网络上已有很多文章阐述了,这里不展开. 本文将从以下几个方面阐述Hybrid app架构设计的一些经验和思考. 原文及讨论请到 github issue 通讯 作 ...

  7. linux c语言 select函数用法

    linux c语言 select函数用法 表头文件 #i nclude<sys/time.h> #i nclude<sys/types.h> #i nclude<unis ...

  8. Struts+Spring+Hibernate、MVC、HTML、JSP

    javaWeb应用 JavaWeb使用的技术,比如SSH(Struts.Spring.Hibernate).MVC.HTML.JSP等等技术,利用这些技术开发的Web应用在政府项目中非常受欢迎. 先说 ...

  9. iOS - XMPP Openfire 服务器的搭建

    前言 提前下载好相关软件,且安装目录最好安装在全英文路径下.如果路径有中文名,那么可能会出现一些莫名其妙的问题. 提前准备好的软件: jdk-8u91-macosx-x64.dmg mysql-5.7 ...

  10. CSS3 Tranform 3D 的应用

    CSS3 Tranform 3D 的应用 一.perspective 属性 1. 作用: 设置元素被查看位置的视图,类似于眼睛到屏幕的距离,一般跟 perspective-origin 共同作用在一个 ...