当前位置: 首页 > 编程笔记 >

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

章禄
2023-03-14
本文向大家介绍Node.js事件循环(Event Loop)和线程池详解,包括了Node.js事件循环(Event Loop)和线程池详解的使用技巧和注意事项,需要的朋友参考一下

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异步运行的魔力。

 类似资料:
  • 问题内容: 首先,我开始尝试了解什么是Node.Js。我有两个问题。 第一个问题 从文章费利克斯的,它说:“只能有一个回调在同一时间发射。在此之前,回调完成执行,所有其他的回调都在排队等候”。 然后,考虑以下代码(从nodejs官方网站复制) 如果同时收到两个客户端请求,则意味着以下工作流程: 收到第一个http请求事件,收到第二个请求事件。 收到第一个事件后,将立即执行第一个事件的回调函数。 同

  • 主要内容:事件驱动程序,实例,Node 应用程序是如何工作的?Node.js 是单进程单线程应用程序,但是因为 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以性能非常高。 Node.js 几乎每一个 API 都是支持回调函数的。 Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。 Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发

  • 问题内容: Node.js I / O事件循环是单线程还是多线程? 如果我有多个I / O进程,则节点会将它们置于外部事件循环中。它们是按顺序处理(最快),还是处理事件循环以同时处理它们(…以及哪些限制)? 问题答案: 事件循环 Node.js事件循环在单个线程下运行,这意味着您编写的应用程序代码在单个线程上进行评估。Nodejs本身在libuv之下使用了许多线程,但是您在编写nodejs代码时不

  • Node.js 是单进程单线程应用程序,但是因为 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以性能非常高。 Node.js 几乎每一个 API 都是支持回调函数的。 Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。 Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发

  • 请说明当netty server和netty client连接成代理时,请求、线程、eventloop、服务器通道和客户端通道之间的关系。在开始之前,我认为对于每个请求,netty服务器都会从获取一个工作线程,以及一个(绑定到工作线程,使用线程池中有限的对象ID)来处理该入站消息,并将其发送到来自netty客户端的出站。此后,可能的服务器<代码>频道受到限制,而客户端<代码>频道受到限制(因为出站

  • 我正在使用vert。x 2.1.5版本。我试图在我的项目中使用事件循环。下面给出了示例代码 在此代码中,我的事件总线在执行事件循环之前返回值。我需要根据事件循环输出填充我的输出 如何实现