Node.js事件循环(Event Loop)和线程池详解


Posted in Javascript onJanuary 28, 2015

Node的“事件循环”(Event Loop)是它能够处理大并发、高吞吐量的核心。这是最神奇的地方,据此Node.js基本上可以理解成“单线程”,同时还允许在后台处理任意的操作。这篇文章将阐明事件循环是如何工作的,你也可以感受到它的神奇。

事件驱动编程

理解事件循环,首先要理解事件驱动编程(Event Driven Programming)。它出现在1960年。如今,事件驱动编程在UI编程中大量使用。JavaScript的一个主要用途是与DOM交互,所以使用基于事件的API是很自然的。

简单地定义:事件驱动编程通过事件或状态的变化来进行应用程序的流程控制。一般通过事件监听实现,一旦事件被检测到(即状态改变)则调用相应的回调函数。听起来很熟悉?其实这就是Node.js事件循环的基本工作原理。

如果你熟悉客户端JavaScript的开发,想一想那些.on*()方法,如element.onclick(),他们用来与DOM元素相结合,传递用户交互。这个工作模式允许在单个实例上触发多个事件。Node.js通过EventEmitter(事件发生器)触发这种模式,如在服务器端的Socket和 “http”模块中。可以从一个单一实例触发一种或一种以上的状态改变。

另一种常见的模式是表达成功succeed和失败fail。现在一般有两种常见的实现方式。首先是将“Error异常”传入回调,一般作为第一个参数传递给回调函数。第二种即使用Promises设计模式,已经加入了ES6。注* Promise模式采用类似jQuery的函数链式书写方式,以避免深层次的回调函数嵌套,如:

$.getJSON('/getUser').done(successHandler).fail(failHandler)

“fs”(filesystem)模块大多采用往回调中传入异常的风格。在技术上触发某些调用,例如fs.readFile()附加事件,但该API只是为了提醒用户,用来表达操作成功或失败。选择这样的API是出于架构的考虑,而非技术的限制。

一个常见的误解是,事件发生器(event emitters)在触发事件时也是天生异步的,但这是不正确的。下面是一个简单的代码片段,以证明这一点。

function MyEmitter() {

  EventEmitter.call(this);

}

util.inherits(MyEmitter, EventEmitter);
MyEmitter.prototype.doStuff = function doStuff() {

  console.log('before')

  emitter.emit('fire')

  console.log('after')}

};
var me = new MyEmitter();

me.on('fire', function() {

  console.log('emit fired');

});
me.doStuff();

// 输出:

// before

// emit fired

// after
注* 如果 emitter.emit 是异步的,则输出应该为

// before

// after

// emit fired

EventEmitter经常表现地很异步,因为它经常用于通知需要异步完成的操作,但EventEmitter API本身是完全同步的。监听函数内部可以按异步执行,但请注意,所有的监听函数将按被添加的顺序同步执行。

机制概述和线程池

Node本身依赖多个库。其中之一是libuv,神奇的处理异步事件队列和执行的库。

Node利用尽可能多的利用操作系统内核实现现有的功能。像生成响应请求(request),转发连接(connections)并委托给系统处理。例如,传入的连接通过操作系统进行队列管理,直到它们可以由Node处理。

您可能听说过,Node有一个线程池,你可能会疑惑:“如果Node会按次序处理任务,为什么还需要一个线程池?”这是因为在内核中,不是所有任务都是按异步执行的。在这种情况下,Node.JS必须能在操作时将线程锁定一段时间,以便它可以继续执行事件循环而不会被阻塞。

下面是一个简单的示例图,来表示他内部的运行机制:

            ┌───────────────────────┐
?──►│         timers                                           │
 │         └───────────┬───────────┘
 │         ┌───────────┴───────────┐
 │         │   pending callbacks                             │
 │         └───────────┬───────────┘          ┌──────────────┐
 │         ┌───────────┴───────────┐          │  incoming:                    │
 │          │          poll                                               │◄──┤ connections,                │
 │         └───────────┬───────────┘          │  data, etc.                     │
 │         ┌───────────┴───────────┐          └──────────────┘
?───┤      setImmediate                                  │
             └───────────────────────┘

关于事件循环的内部运行机制,有一些理解困难的地方:

所有回调都会经由process.nextTick(),在事件循环(例如,定时器)一个阶段的结束并转换到下一阶段之前预设定。这就会避免潜在的递归调用process.nextTick(),而造成的无限循环。
“Pending callbacks(待回调)”,是回调队列中不会被任何其他事件循环周期处理(例如,传递给fs.write)的回调。

Event Emitter 和 Event Loop

通过创建EventEmitter,可简化与事件循环的交互。它是一个通用的封装,可以让你更容易地创建基于事件的API。关于这两者如何互动往往让开发者感到混乱。

下面的例子表明,忘记了事件是同步触发的,可能导致事件被错过。

// v0.10以后,不再需要require('events').EventEmitter 

var EventEmitter = require('events');

var util = require('util');
function MyThing() {

  EventEmitter.call(this);
  doFirstThing();

  this.emit('thing1');

}

util.inherits(MyThing, EventEmitter);
var mt = new MyThing();
mt.on('thing1', function onThing1() {

  // 抱歉,这个事件永远不会发生

});

上面的'thing1'事件,永远不会被MyThing()捕获,因为MyThing()必须在实例化后才能侦听事件。下面的是一个简单的解决方法,不必添加任何额外的闭包:
var EventEmitter = require('events');

var util = require('util');
function MyThing() {

  EventEmitter.call(this);
  doFirstThing();

  setImmediate(emitThing1, this);

}

util.inherits(MyThing, EventEmitter);
function emitThing1(self) {

  self.emit('thing1');

}
var mt = new MyThing();
mt.on('thing1', function onThing1() {

  // 执行了

});

下面的方案也可以工作,不过要损失一些性能:

function MyThing() {

  EventEmitter.call(this);
  doFirstThing();

  // 使用 Function#bind() 会损失性能

  setImmediate(this.emit.bind(this, 'thing1'));

}

util.inherits(MyThing, EventEmitter);

另一个问题是触发Error(异常)。找出您应用程序中的问题已经很难了,但没了调用堆栈(注* e.stack),则几乎不可能调试。当Error被远端的异步请求调用堆栈将丢失。有两个可行的解决方案:同步触发或确保Error跟其他重要信息一起传入。下面的例子演示了这两种解决方案:
MyThing.prototype.foo = function foo() {

  // 这个 error 会被异步触发

  var er = doFirstThing();

  if (er) {

    // 在触发时,需要创建一个新的保留现场调用堆栈信息的error

    setImmediate(emitError, this, new Error('Bad stuff'));

    return;

  }
  // 触发error,马上处理(同步)

  var er = doSecondThing();

  if (er) {

    this.emit('error', 'More bad stuff');

    return;

  }

}

审时度势。当error被触发时,是有可能被立即处理的。或者,它可能是一些琐碎的,可以很容易处理,或在以后再处理的异常。此外通过一个构造函数,传递Error也不是一个好主意,因为构造出来的对象实例很有可能是不完整的。刚才直接抛出Error的情况是个例外。

结束语

这篇文章比较浅显地探讨了有关事件循环的内部运作机制和技术细节。都是经过深思熟虑的。另一篇文章会讨论事件循环与系统内核的交互,并展现NodeJS异步运行的魔力。

Javascript 相关文章推荐
Javascript JSQL,SQL无处不在,
May 05 Javascript
js Event对象的5种坐标
Sep 12 Javascript
用JavaScript实现用一个DIV来包装文本元素节点
Sep 09 Javascript
jquery渐隐渐显的图片幻灯闪烁切换实现方法
Feb 26 Javascript
JS实现下拉菜单赋值到文本框的方法
Aug 18 Javascript
jquery背景跟随鼠标滑动导航
Nov 20 Javascript
深入探讨Vue.js组件和组件通信
Sep 12 Javascript
微信js-sdk地理位置接口用法示例
Oct 12 Javascript
JavaScript实现汉字转换为拼音的库文件示例
Dec 22 Javascript
使用AngularJS2中的指令实现按钮的切换效果
Mar 27 Javascript
AngularJS中ng-class用法实例分析
Jul 06 Javascript
使用Ajax实现无刷新上传文件
Apr 12 Javascript
使用Sticker.js实现贴纸效果
Jan 28 #Javascript
javascript实现瀑布流自适应遇到的问题及解决方案
Jan 28 #Javascript
7个让JavaScript变得更好的注意事项
Jan 28 #Javascript
简单谈谈javascript代码复用模式
Jan 28 #Javascript
JS动态添加Table的TR,TD实现方法
Jan 28 #Javascript
扒一扒JavaScript 预解释
Jan 28 #Javascript
javascript弹出页面回传值的方法
Jan 28 #Javascript
You might like
php删除文本文件中重复行的方法
2015/04/28 PHP
迪菲-赫尔曼密钥交换(Diffie?Hellman)算法原理和PHP实现版
2015/05/12 PHP
PHP切割汉字的常用方法实例总结
2019/04/27 PHP
PHP常用的类封装小结【4个工具类】
2019/06/28 PHP
PHP切割整数工具类似微信红包金额分配的思路详解
2019/09/18 PHP
Display SQL Server Version Information
2007/06/21 Javascript
EasyUi tabs的高度与宽度根据IE窗口的变化自适应代码
2010/10/26 Javascript
IE6浏览器中window.location.href无效的解决方法
2014/11/20 Javascript
JavaScript中iframe实现局部刷新的几种方法汇总
2016/01/06 Javascript
js数组的五种迭代方法及两种归并方法(推荐)
2016/06/14 Javascript
浅谈vue-lazyload实现的详细过程
2017/08/22 Javascript
JavaScript数组去重的多种方法(四种)
2017/09/19 Javascript
Vue.js 父子组件通信的十种方式
2018/10/30 Javascript
layui问题之模拟table表格中的选中按钮选中事件的方法
2019/09/20 Javascript
[04:42]5分钟带你了解什么是DOTA2(第一期)
2017/02/07 DOTA
[48:26]VGJ.S vs infamous Supermajor 败者组 BO3 第二场 6.4
2018/06/05 DOTA
Python的Django框架中从url中捕捉文本的方法
2015/07/20 Python
Python 功能和特点(新手必学)
2015/12/30 Python
详解python列表生成式和列表生成式器区别
2019/03/27 Python
Python Django 实现简单注册功能过程详解
2019/07/29 Python
Python操作MySQL数据库实例详解【安装、连接、增删改查等】
2020/01/17 Python
python+excel接口自动化获取token并作为请求参数进行传参操作
2020/11/10 Python
使用CSS实现阅读进度条
2017/02/27 HTML / CSS
html5的画布canvas——画出弧线、旋转的图形实例代码+效果图
2013/06/09 HTML / CSS
HTML5标签嵌套规则详解【必看】
2016/04/26 HTML / CSS
Stuart Weitzman美国官网:美国奢华鞋履品牌
2016/08/18 全球购物
Hotels.com台湾:饭店订房网
2017/09/06 全球购物
美国转售二手商品的电子商务平台:BLINQ
2018/12/13 全球购物
编辑个人求职信范文
2013/09/21 职场文书
装修五一活动策划案
2014/01/23 职场文书
优秀的个人求职信范文
2014/05/09 职场文书
病媒生物防治方案
2014/05/13 职场文书
2014世界杯球队球队口号
2014/06/05 职场文书
优秀本科毕业生自荐信
2014/07/04 职场文书
幼儿园中秋节活动总结
2015/03/23 职场文书
nginx反向代理配置去除前缀案例教程
2021/07/26 Servers