Node.js事件循环

Node核心功能是事件循环,此概念也多用于JS底层行为以及许多交互系统中。在许多语言中,事件模型是在外层的,但JS事件一直是其语言的核心模块,这是因为JS在很多场景下都需要处理与用户交互的事件。在服务端没有网页DOM对应的那些有限的用户驱动型交互事件,而是在服务器程序上对应发生的各种不同的事件,例如HTTP服务器模块在用户发送请求给web服务器时会触发request事件。

JS利用事件循环来合理地处理系统各部分的请求。Node采用的方式是,所有I/O事件都应该是非阻塞的。这意味着需要让程序暂停操作的HTTP请求、数据库查询、文件读写,以及其他事情,在数据返回之前并不暂停执行。这些事件都将独立运行,然后在数据准备好后触发一个事件。也就是说Node编程会用到很多回调函数,来处理各种I/O。回调函数往往以级联的方式嵌入在其他回调函数中,这与浏览器编程有所不同。除了用顺序的方式设置好启动项外,大部分代码都在处理回调函数。

针对这种少见的编程风格,我们需要寻找合适服务器编程的处理模式。先从事件模型开始,大部分人直觉上是理解事件驱动编程的,因为这和日常生活息息相关。

事件驱动的人们

假设你在烧饭,正在切青椒的时候锅里面的东西开始沸溢出来了,你会暂停切菜把炉火关小。你不会在切青椒的同时把炉火关小,而是会采用更加安全的方式,通过快速切换工作对象来达到同样的目的。事件驱动编程也是同样的道理。通过让程序员一次只能为一个回调函数编写处理代码,可以让代码可读性更强,而且能够快速地处理多个任务。

事件驱动的人们

日常生活中,人们习惯于用各种内部回调的方式来处理遇到的事件。和JS类似,我们一次只能处理一件事情。这也和JS很像,能让事件来驱动操作很棒,但它只能以单线程的方式运行,即同一时间只能处理一件事情。

单线程的概念非常重要,常有人批评Node缺乏并发,也就是没有利用机器上所有CPU来运行JS。但是同时在多个CPU上运行程序也有它的问题,即需要协调多个执行线程。要让多个CPU有效地拆分任务,它们之间需要不停地交换信息,比如当前执行状态以及各自完成了那些工作。虽然这不是不可能,但这么复杂的模型给程序员和系统带来了很大的工作量。JS的方式很简单,同一时刻只有一件事情在操作。Node做的每一件事情都是非阻塞的,所以事件触发与Node对其操作的时间间隔是很短的,因为Node不需要等待诸如磁盘I/O这样的操作。

事件驱动的快递员

以快递员投递为例帮助你理解事件循环。快递员的每个快件都是一个事件,他有一堆快件等着要按顺序处理,每个快件都要走相应的路径进行投递。路径就是对此事件的回调函数。可怜的是,快递员只有一双腿,每次只能走其中一条路径。

事件驱动的快递员

偶尔,当快递员在路上行走时,有人会给它派发另一快件,这就像是投递途中的回调函数。这种情况下,快递员会马上去派送新的快件。此时快递员会立即切换到新的路径去投递。完成后在回到之前的路径上继续工作。

对比快递员的行为和一般程序的做法。假设web服务器被请求要从数据库读取数据,然后返回给用户。这种情况下,我们只要处理很少的事件。首先,用户的请求是要web服务器返回一个网页。处理此个初始请求的回调函数A会先从请求的对象中确定它要从数据库读取什么内容,然后向数据库发起具体的请求,并传入一个回调函数B供请求完成时使用。处理完请求后,回调函数A结束并返回。当数据库找到需要的内容,再触发相应事件。事件循环队列则调用回调函数B,让它把数据发送给用户。

这似乎非常直观,这里需要特别注意的是代码隔断的地方,这也是过程式编程不会遇到的情况。因为Node是一个非阻塞的系统,所以当调用需要阻塞等待的数据库函数时,我们会采用回调函数替代闲置等待。这就是说,由另外一些函数来接管这个请求,并在数据准备好返回时把它处理掉。所以我们需要确定回调函数所要用到的数据能够有办法取得。JS编程通常是利用闭包来实现这个功能的。

事件驱动的快餐店

为什么Node更加高效呢?想象下在快餐店点餐,你站在柜台前排队时,服务员有两种方式来处理你的点单,一种是事件驱动的,另一种则不是。先采用PHP等许多web平台所使用的方式。你点餐时服务员先招待你,待你点完后才服务下一个客人。他输入完你的单子后可以做以下几件事情:收款、为你倒饮料等。但是,服务员还不知道要等多久厨房才能把你的快餐做好。在传统web服务框架下,每个服务程序(线程 )每次只能服务一个请求。唯一增加处理能力的方法是加入更多的线程。很显然这样的做法并不是那么的高效,服务员在等待厨房做菜时浪费了很多时间。

事件驱动的快餐店

显然现实生活的餐馆使用的是更加高效的模式。你点完菜后,服务员会给你一个号码,在菜做好时通知你,你可以称之为回调号码。Node也是这样工作的。当I/O一类费时操作开始时,Node会给它们一个回调引用,然后继续处理其他已经就绪的工作。比如服务员可服务下一个客人,对Node来说则是下一个事件。需要重点关注的是。当呼叫某位客人来取食物的时候,他们不会处理新客人的需求,反之亦然。通过事件驱动的运作方式,服务员能够最大程度地提高产出。

在一些小餐馆,厨师和服务员是同一个人,这种情况下采用事件驱动并不能提高效率,因为所有的工作都由同一个人完成,事件驱动的架构并不能增加价值。如果服务器的全部或大部分工作是进行运算,Node并非最理想的模型。

假设餐馆中有两名服务员和四位客人。如果服务员一次只能服务一位客人,那么头两个客人可最快地拿到食物,而剩余两位的体验会很糟糕。前两位客人之所以能够快速地获得食物是因为服务员在全力满足他们的需求,这占用了后两位客人的时间。在事件驱动模型下,头两位客人可能需稍微等待一下才能拿到食物,因为服务员需先处理一下后两位客人的点单,但系统的平均等待时间(延迟)将大大降低。

被阻塞的邮递员

我们给事件循环模式的邮递员一封信去投递,但投递这封信需要经过一扇门。邮递员达到目的地,而门却关闭着,所以他只能等待并不停地尝试进入。他等待门打开就像进入了死循环模式。如果在信封队列里有另外一封信能够通知某人来打开门,让邮递员进去,这不就解决问题了吗?不幸的是,邮递员正在无休止地等待打开门,无法抽身去投递那封信,这是因为打开门的事件在当前回调事件的外部。如果在回调函数内部发起事件,我们知道邮递员会优先把这封信给投递掉哦,但是当事件是在当前执行代码的外部发生时,它必须等待正在执行的代码完成之后才会被调用。

被阻塞的事件队列

虽然我们不太会编写依赖外部条件作为跳出判断的循环体,但这展示了Node同时只能处理一件事情的本质,任何一点缺陷都可能导致整个系统混乱。这也是事件驱动编程的核心模块是非阻塞I/O的原因。

var evt = require('events');
var EE = evt.EventEmitter;
var ee = new EE();

var die = false;

ee.on('die', function(){
  die = true;
});

setTimeout(function(){
  ee.emit('die');
}, 100);

while(!die){}

// console.log()永远不会被调用,因为while循环不会让Node有机会触发timeout回调函数并发起die事件。
console.log('done');

创建HTTP服务器

var http = require('http');

http.createServer(function(req, res){
  res.writeHead(200, {Content-Type:'text/plain'});
  res.end('Hi\n');
}).listen(8124, '127.0.0.1');

console.log('server running');

通过调用http库的工厂方法来创建HTTP服务器,工厂方法在创建新的HTTP服务器的同时,为request事件绑定了一个回调函数,后者作为createServer()的第一个参数传递进去。当代码运行的时候会发生什么有趣的事情呢?

Node.js运行的第一件事情是把代码从头到尾运行一遍,这可以认为是Node编程的设置阶段。因为我们绑定了一些事件监听器,所以Node.js不会退出,而是等待这些事件被触发。若没有绑定任何事件,Node.js在运行完代码后就会立即退出。

当服务器接收到一个HTTP请求时会进行怎样的处理呢?Node.js会发起request事件,因为该事件有对应的回调函数绑定在上面,回调函数会被依次调用。

假设网站变得非常受欢迎,同时又很多请求进来。假设回调函数需执行1秒。在第一个请求后紧跟着又来了第二个请求,那么第二个请求将不会在这1秒内被处理。显然1秒其实是很长的时间了。

让我们来看看真实应用情景,事件循环阻塞的问题会严重的破坏用户体验。HTTP服务器实际上是由操作系统内核处理与客户端的TCP连接的。所以尽管不会恶化到拒绝新连接的境地,但仍然会有这些链接不被处理的危险。为了处理这些问题,希望尽量保持Node.js的事件驱动和非阻塞的特性。同样的方式,让费时的I/O事件回调的方法来通知Node.js,只有数据已经准备好了,才可以进行下一步操作。Node.js程序本身需要把每个回调函数都写得运行迅速,防止把事件循环给阻塞住。

这意味着编写Node.js服务器程序的时候需遵循以下两个策略:

  • 在设置完成以后所有的操作都是事件驱动的
  • 如果Node.js需长时间处理数据就要考虑把它分配给web worker去处理
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,671评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,442评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,524评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,623评论 1 275
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,642评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,584评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,953评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,621评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,865评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,608评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,698评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,378评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,958评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,940评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,173评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,419评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,425评论 2 342

推荐阅读更多精彩内容

  • Node.js 是单进程单线程应用程序,但是通过事件和回调支持并发,所以性能非常高。 Node.js 的每一个 A...
    Junting阅读 327评论 0 2
  • 关键词:Event Loop Node.js 使用事件驱动模型,当web server接收到请求,就把它关闭然后进...
    ferrint阅读 289评论 0 1
  • node.js是单进程单线程应用程序,通过事件和回调支持并发。node.js的没一个API都是异步的,并作为一个独...
    Amy莫莫阅读 400评论 0 2
  • topics: 1.The Node.js philosophy 2.The reactor pattern 3....
    宫若石阅读 1,052评论 0 1
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    Myselfyan阅读 4,062评论 2 58