一、多线程与单线程

像java、python这个可以具有多线程的语言。多线程同步模式是这样的,将cpu分成几个线程,每个线程同步运行。

而node.js采用单线程异步非阻塞模式,也就是说每一个计算独占cpu,遇到I/O请求不阻塞后面的计算,当I/O完成后,以事件的方式通知,继续执行计算2。

事件驱动、异步、单线程、非阻塞I/O,这是我们听得最多的关于nodejs的介绍。看到上面的关键字,可能我们会好奇:

为什么在浏览器中运行的 Javascript 能与操作系统进行如此底层的交互?

nodejs既然是单线程,如何实现异步、非阻塞I/O?

nodejs全是异步调用和非阻塞I/O,就真的不用管并发数了吗?

nodejs事件驱动是如何实现的?和浏览器的event loop是一回事吗?

nodejs擅长什么?不擅长什么?

二、nodejs内部揭秘

要弄清楚上面的问题,首先要弄清楚nodejs是怎么工作的。

我们可以看到,Node.js 的结构大致分为三个层次:

1、 Node.js 标准库,这部分是由 Javascript 编写的,即我们使用过程中直接能调用的 API。在源码中的 lib 目录下可以看到。

2、 Node bindings,这一层是 Javascript 与底层 C/C++ 能够沟通的关键,前者通过 bindings 调用后者,相互交换数据。

3、这一层是支撑 Node.js 运行的关键,由 C/C++ 实现。

V8:Google 推出的 Javascript VM,也是 Node.js 为什么使用的是 Javascript 的关键,它为 Javascript 提供了在非浏览器端运行的环境,它的高效是 Node.js 之所以高效的原因之一。

Libuv:它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,是 Node.js 如此强大的关键。

C-ares:提供了异步处理 DNS 相关的能力。

http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。

三、libuv简介

可以看出,几乎所有和操作系统打交道的部分都离不开 libuv的支持。libuv也是node实现跨操作系统的核心所在。

四、我们再来看看最开始我抛出的问题

问题一:为什么在浏览器中运行的 Javascript 能与操作系统进行如此底层的交互?

举个简单的例子,我们想要打开一个文件,并进行一些操作,可以写下面这样一段代码:

var fs = require('fs');

fs.open('./test.txt', "w", function(err, fd) {

//..do something

});

fs.open = function(path, flags, mode, callback) {

// ...

binding.open(pathModule._makeLong(path),

stringToFlags(flags),

mode,

callback);

};

这段代码的调用过程大致可描述为:lib/fs.js → src/node_file.cc →uv_fs

从JavaScript调用Node的核心模块,核心模块调用C++内建模块,内建模块通过 libuv进行系统调用,这是Node里经典的调用方式。总体来说,我们在 Javascript 中调用的方法,最终都会通过node-bindings 传递到 C/C++ 层面,最终由他们来执行真正的操作。Node.js 即这样与操作系统进行互动。

问题二:nodejs既然是单线程,如何实现异步、非阻塞I/O?

顺便回答标题nodejs真的是单线程吗?其实只有js执行是单线程,I/O显然是其它线程。

js执行线程是单线程,把需要做的I/O交给libuv,自己马上返回做别的事情,然后libuv在指定的时刻回调就行了。其实简化的流程就是酱紫的!细化一点,nodejs会先从js代码通过node-bindings调用到C/C++代码,然后通过C/C++代码封装一个叫 “请求对象” 的东西交给libuv,这个请求对象里面无非就是需要执行的功能+回调之类的东西,给libuv执行以及执行完实现回调。

总结来说,一个异步 I/O 的大致流程如下:

1、发起 I/O 调用

用户通过 Javascript 代码调用 Node 核心模块,将参数和回调函数传入到核心模块;

Node 核心模块会将传入的参数和回调函数封装成一个请求对象;

将这个请求对象推入到 I/O 线程池等待执行;

Javascript 发起的异步调用结束,Javascript 线程继续执行后续操作。

2、执行回调

I/O 操作完成后,会取出之前封装在请求对象中的回调函数,执行这个回调函数,以完成 Javascript 回调的目的。(这里回调的细节下面讲解)

从这里,我们可以看到,我们其实对 Node.js 的单线程一直有个误会。事实上,它的单线程指的是自身 Javascript 运行环境的单线程,Node.js 并没有给 Javascript 执行时创建新线程的能力,最终的实际操作,还是通过 Libuv 以及它的事件循环来执行的。这也就是为什么 Javascript 一个单线程的语言,能在 Node.js 里面实现异步操作的原因,两者并不冲突。

问题三:nodejs全是异步调用和非阻塞I/O,就真的不用管并发数了吗?

之前我们就提到了线程池的概念,发现nodejs并不是单线程的,而且还有并行事件发生。同时,线程池默认大小是 4 ,也就是说,同时能有4个线程去做文件i/o的工作,剩下的请求会被挂起等待直到线程池有空闲。 所以nodejs对于并发数,是由限制的。

线程池的大小可以通过 UV_THREADPOOL_SIZE 这个环境变量来改变 或者在nodejs代码中通过 process.env.UV_THREADPOOL_SIZE来重新设置。

问题四:nodejs事件驱动是如何实现的?和浏览器的event loop是一回事吗?

event loop是一个执行模型,在不同的地方有不同的实现。浏览器和nodejs基于不同的技术实现了各自的event loop。

简单来说:

nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。

libuv已经对event loop作出了实现,而html5规范中只是定义了浏览器中event loop的模型,具体实现留给了浏览器厂商。

我们上面提到了libuv接过了js传递过来的 I/O请求,那么何时来处理回调呢?

libuv有一个事件循环(event loop)的机制,来接受和管理回调函数的执行。

event loop是libuv的核心所在,上面我们提到 js 会把回调和任务交给libuv,libuv何时来调用回调就是 event loop 来控制的。event loop 首先会在内部维持多个事件队列(或者叫做观察者 watcher),比如 时间队列、网络队列等等,使用者可以在watcher中注册回调,当事件发生时事件转入pending状态,再下一次循环的时候按顺序取出来执行,而libuv会执行一个相当于 while true的无限循环,不断的检查各个watcher上面是否有需要处理的pending状态事件,如果有则按顺序去触发队列里面保存的事件,同时由于libuv的事件循环每次只会执行一个回调,从而避免了 竞争的发生。Libuv的 event loop执行图:

nodejs的event loop分为6个阶段,每个阶段的作用如下:

timers:执行setTimeout() 和 setInterval()中到期的callback。

I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行

idle, prepare:仅内部使用

poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段

check:执行setImmediate的callback

close callbacks:执行close事件的callback,例如socket.on("close",func)

event loop的每一次循环都需要依次经过上述的阶段。 每个阶段都有自己的callback队列,每当进入某个阶段,都会从所属的队列中取出callback来执行,当队列为空或者被执行callback的数量达到系统的最大数量时,进入下一阶段。这六个阶段都执行完毕称为一轮循环。

附带event loop 源码:

int uv_run(uv_loop_t* loop, uv_run_mode mode) {

int timeout;

int r;

int ran_pending;

/*

从uv__loop_alive中我们知道event loop继续的条件是以下三者之一:

1,有活跃的handles(libuv定义handle就是一些long-lived objects,例如tcp server这样)

2,有活跃的request

3,loop中的closing_handles

*/

r = uv__loop_alive(loop);

if (!r)

uv__update_time(loop);

while (r != 0 && loop->stop_flag == 0) {

uv__update_time(loop);//更新时间变量,这个变量在uv__run_timers中会用到

uv__run_timers(loop);//timers阶段

ran_pending = uv__run_pending(loop);//从libuv的文档中可知,这个其实就是I/O callback阶段,ran_pending指示队列是否为空

uv__run_idle(loop);//idle阶段

uv__run_prepare(loop);//prepare阶段

timeout = 0;

/**

设置poll阶段的超时时间,以下几种情况下超时会被设为0,这意味着此时poll阶段不会被阻塞,在下面的poll阶段我们还会详细讨论这个

1,stop_flag不为0

2,没有活跃的handles和request

3,idle、I/O callback、close阶段的handle队列不为空

否则,设为timer阶段的callback队列中,距离当前时间最近的那个

**/

if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)

timeout = uv_backend_timeout(loop);

uv__io_poll(loop, timeout);//poll阶段

uv__run_check(loop);//check阶段

uv__run_closing_handles(loop);//close阶段

//如果mode == UV_RUN_ONCE(意味着流程继续向前)时,在所有阶段结束后还会检查一次timers,这个的逻辑的原因不太明确

if (mode == UV_RUN_ONCE) {

uv__update_time(loop);

uv__run_timers(loop);

}

r = uv__loop_alive(loop);

if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)

break;

}

if (loop->stop_flag != 0)

loop->stop_flag = 0;

return r;

}

这里我们再详细了解一下poll阶段:

poll 阶段有两个主要功能:

1、执行下限时间已经达到的timers的回调

2、处理 poll 队列里的事件。

当event loop进入 poll 阶段,并且 没有设定的timers(there are no timers scheduled),会发生下面两件事之一:

1、如果 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;

2、如果 poll 队列为空,则发生以下两件事之一:

(1)如果代码已经被setImmediate()设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里的回调)。

(2)如果代码没有被setImmediate()设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。

但是,当event loop进入 poll 阶段,并且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态):

event loop将检查timers,如果有1个或多个timers的下限时间已经到达,event loop将绕回 timers 阶段。

event loop的一个例子讲述:

var fs = require('fs');

function someAsyncOperation (callback) {

// 假设这个任务要消耗 95ms

fs.readFile('/path/to/file', callback);

}

var timeoutScheduled = Date.now();

setTimeout(function () {

var delay = Date.now() - timeoutScheduled;

console.log(delay + "ms have passed since I was scheduled");

}, 100);

// someAsyncOperation要消耗 95 ms 才能完成

someAsyncOperation(function () {

var startCallback = Date.now();

// 消耗 10ms...

while (Date.now() - startCallback < 10) {

; // do nothing

}

});

当event loop进入 poll 阶段,它有个空队列(fs.readFile()尚未结束)。所以它会等待剩下的毫秒,直到最近的timer的下限时间到了。当它等了95ms,fs.readFile()首先结束了,然后它的回调被加到 poll的队列并执行——这个回调耗时10ms。之后由于没有其它回调在队列里,所以event loop会查看最近达到的timer的下限时间,然后回到 timers 阶段,执行timer的回调。

所以在示例里,回调被设定 和 回调执行间的间隔是105ms。

到这里我们再总结一下,整个异步IO的流程:

问题五、nodejs擅长什么?不擅长什么?

Node.js 通过 libuv 来处理与操作系统的交互,并且因此具备了异步、非阻塞、事件驱动的能力。因此,NodeJS能响应大量的并发请求。所以,NodeJS适合运用在高并发、I/O密集、少量业务逻辑的场景。

上面提到,如果是 I/O 任务,Node.js 就把任务交给线程池来异步处理,高效简单,因此 Node.js 适合处理I/O密集型任务。但不是所有的任务都是 I/O 密集型任务,当碰到CPU密集型任务时,即只用CPU计算的操作,比如要对数据加解密(node.bcrypt.js),数据压缩和解压(node-tar),这时 Node.js 就会亲自处理,一个一个的计算,前面的任务没有执行完,后面的任务就只能干等着 。我们看如下代码:

var start = Date.now();//获取当前时间戳

setTimeout(function () {

console.log(Date.now() - start);

for (var i = 0; i < 1000000000; i++){//执行长循环

}

}, 1000);

setTimeout(function () {

console.log(Date.now() - start);

}, 2000);

最终我们的打印结果是:(结果可能因为你的机器而不同)

1000

3738

对于我们期望2秒后执行的setTimeout函数其实经过了3738毫秒之后才执行,换而言之,因为执行了一个很长的for循环,所以我们整个Node.js主线程被阻塞了,如果在我们处理100个用户请求中,其中第一个有需要这样大量的计算,那么其余99个就都会被延迟执行。如果操作系统本身就是单核,那也就算了,但现在大部分服务器都是多 CPU 或多核的,而 Node.js 只有一个 EventLoop,也就是只占用一个 CPU 内核,当 Node.js 被CPU 密集型任务占用,导致其他任务被阻塞时,却还有 CPU 内核处于闲置状态,造成资源浪费。

其实虽然Node.js可以处理数以千记的并发,但是一个Node.js进程在某一时刻其实只是在处理一个请求。

因此,Node.js 并不适合 CPU 密集型任务。

为什么nodejs是单进程的_nodejs真的是单线程吗?相关推荐

  1. 为什么nodejs是单进程的_Nodejs探秘:深入理解单线程实现高并发原理

    导语:在我接触Nodejs的时候,听的最多的关键字就是:事件驱动.非阻塞I/O.高效.轻量,是单线程且支持高并发的脚本语言.可为什么单线程的nodejs可以支持高并发呢?很多人都不明白其原理,自己也在 ...

  2. 为什么nodejs是单进程的_Nodejs·进程

    之前对这部分的内容很感兴趣,没想到读起来有点晦涩,还是因为对服务器的知识不是很了解. 说道服务器一般人都会想到tomcat或者Jboss或者weblogic,现在流行起来的Node总让人不太放心,JS ...

  3. nodejs 当前文件路径_NodeJs的几种文件路径

    上次写删除文件夹的时候用到了fs模块,也集中用到了很多种路径,当时就想写一下,在Node中使用各种路径的问题,于是就简单写了一下,可以从 这里 获取demo源代码. 刚写Node的时候经常会遇到这种情 ...

  4. nodejs 获取文件路径_Nodejs读取文件时相对路径的正确写法(使用fs模块)

    在开发Nodejs中,我们往往最常用的模块就是fs核心模块(fs.readFile)来读取文件.代码如下: 但是运行之后,并没有按照想象中一样,读取test.html文件内容,这是一个bug,坑爹的玩 ...

  5. nodejs操作sqlserver数据_nodejs基于mssql模块连接sqlserver数据库的简单封装操作示例...

    本文实例讲述了nodejs基于mssql模块连接sqlserver数据库的简单封装操作.分享给大家供大家参考,具体如下: 注意:开启sqlserver服务器允许远程连接的步骤,自行百度,很多经验,no ...

  6. nodejs的mysql模块_Nodejs的mysql模块学习(一)

    介绍 mysql npm包 是一个nodejs的模块,由JavaScript编写 安装 npm install mysql 建立连接 var mysql = require('mysql');//引用 ...

  7. nodejs进入mysql数据库_nodejs简单访问及操作mysql数据库的方法示例

    本文实例讲述了nodejs简单访问及操作mysql数据库的方法.分享给大家供大家参考,具体如下: var mysql = require('mysql'); //调用MySQL模块 mysql模块要安 ...

  8. nodejs json转对象_nodejs读取xlsx格式文件

    # nodejs读取xlsx格式文件 安装 npm i node-xlsx -D // 或者 yarn add node-xlsx -D 使用 读取表格数据,并生成json对象 /*** @auth ...

  9. nodejs mysql 耗硬盘_nodejs操作MySQL其实很简单

    本文概要 讲述了"如何回家取酱油瓶"跟连接数据库的关联. 2.使用sequelize.js (一个传说中的ORM技术:Object-Relational Mapping,能把关系数 ...

最新文章

  1. android 组装xml,Android中组装xml文件内容
  2. 为什么下一个SaaS公司绝不会效仿Salesforce?
  3. 初等数论--同余方程--同余方程组:中国剩余定理
  4. 3.Hadoop的学习(ubuntu安装配置jdk)
  5. Archive for required library: ‘WebContent/WEB-INF/lib/xxx.jar cannotn
  6. 滴滴为啥值3600亿?看它的数据中台就知道了
  7. 树的存储结构-双亲表示法
  8. 配置.net连接数据库的配置文件
  9. 枚举算法:试统计正整数n的阶层n!=1*2*...*n尾部连续零的个数。
  10. java的连接 初始化_java类从加载、连接到初始化过程详解
  11. html向上移动图片代码,图片随网页上下移动的代码实例
  12. NOIP模拟——偷书(读书人的事,那能叫偷吗?)
  13. Spring Boot (Vue3+ElementPlus+Axios+MyBatisPlus +Spring Boot 前后端分离)
  14. 电脑输入法不见了怎么解决
  15. 傻子也能看懂的迪杰斯特拉算法(转)
  16. 我们需要培养职业化的工作习惯
  17. C#压缩、解压缩文件(夹)(rar、zip)
  18. 网页中通过js修改img的src属性刷新图片时,图片缓存问题现象表述及问题解决
  19. C++ cout输出中文
  20. 游戏服务器 协议安全,游戏服务器 协议安全

热门文章

  1. 2.2.5 Adam优化算法
  2. 3号团队-团队任务4:每日例会(2018-12-3)
  3. ubuntu下 将证书导入java的cacerts证书库
  4. SecureCR 改变背景色和文字颜色
  5. P1121 环状最大两段子段和
  6. 【python之路24】装饰器
  7. 【转】TI蓝牙BLE 协议栈代码学习
  8. [android] Serializable 和 Parcelable 区别
  9. 新人新气象,给自己加油!
  10. 自己去年用intraweb写的模仿动网论坛的原程序,用的是动网论坛的数据库