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. CentOS7下安装rabbitmq消息队

    安装如下步骤: 1.首先安装erlang yum install erlang 注:如果出现No package erlang available. rpm -Uvh http://download. ...

  2. Linux时间子系统之二:表示时间的单位和结构

    人们习惯用于表示时间的方法是:年.月.日.时.分.秒.毫秒.星期等等,但是在内核中,为了软件逻辑和代码的方便性,它使用了一些不同的时间表示方法,并为这些表示方法定义了相应的变量和数据结构,本节的内容就 ...

  3. BigDecimal 专题

    //****BigDecimal中传入的double类型的数据,要为String类型,不然得到在BigDecimal仍然是不准确的double数据**** // BigDecimal addend = ...

  4. PHP异步请求

    正常情况下,PHP都是同步请求,脚本右上而下依次执行,必须等上一步请求好了,才能进行下一步操作,这种效率在某些时候是不必要的,如发送邮件等操作,是可以异步处理的. PHP异步也很不少插件,我们使用的是 ...

  5. FPGA学习笔记(四)——Verilog基本语法

    ###### [该随笔部分内容转载自小梅哥] ######### 组合逻辑:    多路选择器.加法器.译码器.乘法器 时序逻辑:    计数器.分频器.定时器.移位寄存器 一.Verilog文件的基 ...

  6. selenium中的xpath定位

    一.简介 XPath 是一门在 XML 文档中查找信息的语言.XPath 用于在 XML 文档中通过元素和属性进行导航. 二.定位 2.1 利用自己的本身属性定位 //标签[文本属性和值] 下面百度的 ...

  7. DUBBO报错分析—1(连接zookeeper成功,调用方法无反应,不报错)

    思路分析 调用方法时,最后调用执行的是mapper的sql语句,既然调用对应的方法无法获取返回值,多是sql错误,但是并未报sql错误,说明可能是与sql相关的数据源配置错误. 过程调试 经尝试,当直 ...

  8. 跨域问题实践总结!下( [HTML5] postMessage+服务器端(反向代理服务器+CORS Cross-Origin Resource Sharing))

    4. [HTML5] postMessage 问题: 对于跨域问题,研究了一下html5的postMessage,写了代码测试了一下,感觉html5新功能就是好用啊.此文仅使用html5的新特性pos ...

  9. volume.go

    package)) ) ,) ) );) ,     } }

  10. BZOJ_4551_[Tjoi2016&Heoi2016]树_树剖+线段树

    BZOJ_4551_[Tjoi2016&Heoi2016]树_树剖+线段树 Description 在2016年,佳媛姐姐刚刚学习了树,非常开心.现在他想解决这样一个问题:给定一颗有根树(根为 ...