nodejs真的是单线程吗?

[原文

1、多线程与单线程javascript

像java、python这个能够具备多线程的语言。多线程同步模式是这样的,将cpu分红几个线程,每一个线程同步运行。html

图片描述

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

图片描述

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

为何在浏览器中运行的 Javascript 能与操做系统进行如此底层的交互?
nodejs既然是单线程,如何实现异步、非阻塞I/O?
nodejs全是异步调用和非阻塞I/O,就真的不用管并发数了吗?
nodejs事件驱动是如何实现的?和浏览器的event loop是一回事吗?
nodejs擅长什么?不擅长什么?node

2、nodejs内部揭秘

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

图片描述

咱们能够看到,Node.js 的结构大体分为三个层次:git

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

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

三、这一层是支撑 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、数据压缩等其余的能力。浏览器

3、libuv简介

图片描述

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

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

问题一:为何在浏览器中运行的 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 的大体流程以下:

一、发起 I/O 调用
用户经过 Javascript 代码调用 Node 核心模块,将参数和回调函数传入到核心模块;
Node 核心模块会将传入的参数和回调函数封装成一个请求对象;
将这个请求对象推入到 I/O 线程池等待执行;
Javascript 发起的异步调用结束,Javascript 线程继续执行后续操做。

二、执行回调
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 阶段有两个主要功能:
一、执行下限时间已经达到的timers的回调
二、处理 poll 队列里的事件。

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

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

二、若是 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的流程:

图片描述

问题5、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 密集型任务。

参考文章:
https://www.cnblogs.com/chris...
https://www.cnblogs.com/onepi...
https://blog.csdn.net/scandly...
http://liyangready.github.io/...
https://blog.csdn.net/xjtrodd...
https://blog.csdn.net/sinat_2...

相关文章
相关标签/搜索