Node.js 采用事件驱动和异步 I/O 的方式,实现了一个单线程、高并发的 JavaScript 运行时环境,而单线程就意味着同一时间只能做一件事,那么 Node.js 如何通过单线程来实现高并发和异步 I/O?本文将围绕这个问题来探讨 Node.js 的单线程模型 。

高并发策略

一般来说,高并发的解决方案就是提供多线程模型,服务器为每个客户端请求分配一个线程,使用同步 I/O,系统通过线程切换来弥补同步 I/O 调用的时间开销。比如 Apache 就是这种策略,由于 I/O 一般都是耗时操作,因此这种策略很难实现高性能,但非常简单,可以实现复杂的交互逻辑。

而事实上,大多数网站的服务器端都不会做太多的计算,它们接收到请求以后,把请求交给其它服务来处理(比如读取数据库),然后等着结果返回,最后再把结果发给客户端。因此,Node.js 针对这一事实采用了单线程模型来处理,它不会为每个接入请求分配一个线程,而是用一个主线程处理所有的请求,然后对 I/O 操作进行异步处理,避开了创建、销毁线程以及在线程间切换所需的开销和复杂性。

事件循环

Node.js 在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。

当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop),其运行原理如下图所示:

这个图是整个 Node.js 的运行原理,从左到右,从上到下,Node.js 被分为了四层,分别是 应用层V8引擎层Node API层 和 LIBUV层。

应用层:   即 JavaScript 交互层,常见的就是 Node.js 的模块,比如 http,fs

V8引擎层:  即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互

NodeAPI层:  为上层模块提供系统调用,一般是由 C 语言来实现,和操作系统进行交互 。

LIBUV层: 是跨平台的底层封装,实现了 事件循环、文件操作等,是 Node.js 实现异步的核心 。

无论是 Linux 平台还是 Windows 平台,Node.js 内部都是通过 线程池 来完成异步 I/O 操作的,而 LIBUV 针对不同平台的差异性实现了统一调用。因此,Node.js 的单线程仅仅是指 JavaScript 运行在单线程中,而并非 Node.js 是单线程。

工作原理

Node.js 实现异步的核心是事件,也就是说,它把每一个任务都当成 事件 来处理,然后通过 Event Loop 模拟了异步的效果,为了更具体、更清晰的理解和接受这个事实,下面我们用伪代码来描述一下其工作原理 。

【1】定义事件队列

既然是队列,那就是一个先进先出 (FIFO) 的数据结构,我们用JS数组来描述,如下:

/**
* 定义事件队列
* 入队:push()
* 出队:shift()
* 空队列:length == 0
*/
globalEventQueue: []

我们利用数组来模拟队列结构:数组的第一个元素是队列的头部,数组的最后一个元素是队列的尾部,push() 就是在队列尾部插入一个元素,shift() 就是从队列头部弹出一个元素。这样就实现了一个简单的事件队列。

【2】定义接收请求入口

每一个请求都会被拦截并进入处理函数,如下所示:

/**
* 接收用户请求
* 每一个请求都会进入到该函数
* 传递参数request和response
*/
processHttpRequest:function(request,response){ // 定义一个事件对象
var event = createEvent({
params:request.params, // 传递请求参数
result:null, // 存放请求结果
callback:function(){} // 指定回调函数
}); // 在队列的尾部添加该事件
globalEventQueue.push(event);
}

这个函数很简单,就是把用户的请求包装成事件,放到队列里,然后继续接收其他请求。

【3】定义 Event Loop

当主线程处于空闲时就开始循环事件队列,所以我们还要定义一个函数来循环事件队列:

/**
* 事件循环主体,主线程择机执行
* 循环遍历事件队列
* 处理非IO任务
* 处理IO任务
* 执行回调,返回给上层
*/
eventLoop:function(){
// 如果队列不为空,就继续循环
while(this.globalEventQueue.length > 0){ // 从队列的头部拿出一个事件
var event = this.globalEventQueue.shift(); // 如果是耗时任务
if(isIOTask(event)){
// 从线程池里拿出一个线程
var thread = getThreadFromThreadPool();
// 交给线程处理
thread.handleIOTask(event)
}else {
// 非耗时任务处理后,直接返回结果
var result = handleEvent(event);
// 最终通过回调函数返回给V8,再由V8返回给应用程序
event.callback.call(null,result);
}
}
}

主线程不停的检测事件队列,对于 I/O 任务,就交给线程池来处理,非 I/O 任务就自己处理并返回。

【4】处理 I/O 任务

线程池接到任务以后,直接处理IO操作,比如读取数据库:

/**
* 处理IO任务
* 完成后将事件添加到队列尾部
* 释放线程
*/
handleIOTask:function(event){
//当前线程
var curThread = this; // 操作数据库
var optDatabase = function(params,callback){
var result = readDataFromDb(params);
callback.call(null,result)
}; // 执行IO任务
optDatabase(event.params,function(result){
// 返回结果存入事件对象中
event.result = result; // IO完成后,将不再是耗时任务
event.isIOTask = false; // 将该事件重新添加到队列的尾部
this.globalEventQueue.push(event); // 释放当前线程
releaseThread(curThread)
})
}

当 I/O 任务完成以后就执行回调,把请求结果存入事件中,并将该事件重新放入队列中,等待循环,最后释放当前线程,当主线程再次循环到该事件时,就直接处理了。

总结以上过程我们发现,Node.js 只用了一个主线程来接收请求,但它接收请求以后并没有直接做处理,而是放到了事件队列中,然后又去接收其他请求了,空闲的时候,再通过 Event Loop 来处理这些事件,从而实现了异步效果,当然对于IO类任务还需要依赖于系统层面的线程池来处理。

因此,我们可以简单的理解为:Node.js 本身是一个多线程平台,而它对 JavaScript 层面的任务处理是单线程的。

CPU密集型是短板

至此,对于 Node.js 的单线程模型,我们应该有了一个简单而又清晰的认识,它通过事件驱动模型实现了高并发和异步 I/O,然而也有 Node.js 不擅长做的事情:

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

在事件队列中,如果前面的 CPU 计算任务没有完成,后面的任务就会被阻塞,出现响应缓慢的情况,如果操作系统本身就是单核,那也就算了,但现在大部分服务器都是多 CPU 或多核的,而 Node.js 只有一个 EventLoop,也就是只占用一个 CPU 内核,当 Node.js 被CPU 密集型任务占用,导致其他任务被阻塞时,却还有 CPU 内核处于闲置状态,造成资源浪费。

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

适用场景

RESTful API - 请求和响应只需少量文本,并且不需要大量逻辑处理, 因此可以并发处理数万条连接。

聊天服务 - 轻量级、高流量,没有复杂的计算逻辑。

原创发布  @一像素  2017.07  

参考文献:

[1] Node.js软肋之CPU密集型任务

[2] nodejs笔记之:事件驱动,线程池,非阻塞,异常处理等

[3] Node.js机制及原理理解初步

Node.js 事件循环机制的更多相关文章

  1. Node.js 事件循环(Event Loop)介绍

    Node.js 事件循环(Event Loop)介绍 JavaScript是一种单线程运行但又绝不会阻塞的语言,其实现非阻塞的关键是“事件循环”和“回调机制”.Node.js在JavaScript的基 ...

  2. 6、Node.js 事件循环

    #########################################################################################Node.js 事件循 ...

  3. Node.js事件循环

    Node JS是单线程应用程序,但它通过事件和回调概念,支持并发. 由于Node JS每一个API是异步的,作为一个单独的线程,它使用异步函数调用,以保持并发性.Node JS使用观察者模式.Node ...

  4. js事件循环机制辨析

     对于新接触js语言的人来说,最令人困惑的大概就是事件循环机制了.最开始这也困惑了我好久,花了我几个月时间通过书本,打代码,查阅资料不停地渐进地理解他.接下来我想要和大家分享一下,虽然可能有些许错误的 ...

  5. JS 事件循环机制 - 任务队列、web API、JS主线程的相互协同

    一.JS单线程.异步.同步概念 从上一篇说明vue nextTick的文章中,多次出现“事件循环”这个名词,简单说明了事件循环的步骤,以便理解nextTick的运行时机,这篇文章将更为详细的分析下事件 ...

  6. js 事件循环机制 EventLoop

    js 的非阻塞I/O  就是由事件循环机制实现的 众所周知  js是单线程的 也就是上一个任务完成后才能开始新的任务 那js碰到ajxa和定时器.promise这些异步任务怎么办那?这时候就出现了事件 ...

  7. The Node.js Event Loop, Timers, and process.nextTick() Node.js事件循环,定时器和process.nextTick()

    个人翻译 原文:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ The Node.js Event Loop, Ti ...

  8. Node.js 事件循环

    Node.js 是单进程单线程应用程序,但是通过事件和回调支持并发,所以性能非常高. Node.js 的每一个 API 都是异步的,并作为一个独立线程运行,使用异步函数调用,并处理并发. Node.j ...

  9. Node.js 学习(五)Node.js 事件循环

    Node.js 是单进程单线程应用程序,但是通过事件和回调支持并发,所以性能非常高. Node.js 的每一个 API 都是异步的,并作为一个独立线程运行,使用异步函数调用,并处理并发. Node.j ...

随机推荐

  1. 传统IT公司/创业公司/互联网大公司的offer如何选择?[转载+原创]

    背景介绍: 第一家工作的公司是一家跨国外企安全公司, 骄傲的称自己不是互联网公司而是传统软件公司, 第二家公司是当下最热的知识分享社区, 创业公司. 第三家公司是挤走谷歌, 曾一度称霸中国的搜索引擎公 ...

  2. Supported method argument types Spring MVC

    Supported method argument types The following are the supported method arguments: Request or respons ...

  3. HttpClient 专题

    HttpClient is a HTTP/1.1 compliant HTTP agent implementation based on HttpCore. It also provides reu ...

  4. RCTF 2018线上赛 writeup

    苦逼的RCTF,只进行了两天,刚好第二天是5.20,出去xxx了,没法打比赛,难受.比赛结束了,还不准继续提交flag进行正确校验了,更难受. 下面是本次ctf解题思路流程 后面我解出的题会陆续更新上 ...

  5. 让站点支持MarkDown语法~(转)

    Markdown是一种可以使用普通文本编辑器编写的标记语言,通过类似HTML的标记语法,它可以使普通文本内容具有一定的格式.Markdown的语法简洁明了.学习容易,而且功能比纯文本更强,因此有很多人 ...

  6. 在centos上面安装phantomjs

    在opt目录下创建phantomjs文件夹 mkdir -p /opt/phantomjs 把phantomjs解压出来的的文件放到/opt/phantomjs下面 建立软链接 ln -s /opt/ ...

  7. TestNG深入理解

    以下内容引自: http://blog.csdn.net/wanglha/article/details/42004695 TestNG深入理解 转载 2014年12月18日 13:56:11 参考文 ...

  8. BZOJ_3573_[Hnoi2014]米特运输_树形DP+hash

    BZOJ_3573_[Hnoi2014]米特运输_树形DP+hash 题意: 给你一棵树每个点有一个权值,要求修改最少的权值,使得每个节点的权值等于其儿子的权值和且儿子的权值都相等. 分析: 首先我们 ...

  9. Ubuntu配置tomcat9

    buntu 安装jdk:[链接] Ubuntu安装eclipse:[链接] Ubuntu下安装MySQL与mysql workbench:[链接] Ubuntu配置tomcat9:[链接] Ubunt ...

  10. esxi存储(外部共享存储)

    vSphere 基础物理架构中存储是一个非常关键的部分,没有好的存储,虚拟化也就没有存在的价值,并且它能够决定其系统性能的高低和如vMotion等高级功能能否实现.所以本次重点介绍vSphere中的存 ...