日期:2014-05-16  浏览次数:20385 次

理解Node.js的事件循环

一篇讲解node.js事件循环的文章。原文出处:点击跳转!

在了解node.js之前你首先需要了解的一个基本的论点是:I/O是“昂贵”的。


因此对于当前的编程技术而言,最大的浪费来自于等待I/O的完成。下面列出了改善该问题的几种方式,其中的某个可以帮助你提高性能:

  • 同步:在某一时刻,一次只处理一个请求。但这种情况下,任何一个请求都会“耽误”(阻塞)所有其他的请求。
  • fork一个新进程:对于每个请求,你启动一个新的进程来处理。这种情况下,无法达到很好的扩展,上百个连接就意味着上百个进程的存在。fork()函数是Unix程序员的锤子,因为使用它很方便,所以每个程序都看起来像个钉子一样(都喜欢用锤子拿来敲敲它)。所以,经常造成过度使用,而有些过往矫正。
  • 线程:开启一个新的线程来处理每个请求。这种方式很简单,并且对于内核来讲使用线程也比fork进程来得“亲切”,因为通常线程花费比进程更少的开销。缺点:你的机子可能不支持基于线程编程,并且基于线程的程序,其复杂度增长得非常快,同时你还会有对访问共享资源的担忧。

你需要了解的第二个论点是:被线程处理的每个连接都是“内存昂贵的”。

Apache是采用多线程处理请求的。它对于每个请求“孵化”出一个线程(或者进程,这取决于配置)来处理。你将会看到随着并发连接数的增长以及更多的线程需要服务多个客户端时,那些开销有多消耗内存。Nginx跟Node.js都不是基于多线程模型的,因为线程跟进程都需要非常大的内存开销。他们都是单线程的,但是基于事件的。这种基于单线程的模型消除了为了处理很多请求而创建成百上千个线程或进程带来的开销。


Node.js为你的代码保持单线程的运行环境


它确实是基于单线程运行的,你无法编写任何代码来执行并发;例如执行一个"sleep"操作将阻塞整个服务器1秒钟。

while(new Date().getTime() < now + 1000) {
   // do nothing
}

因此,当代码运行的时候,node.js将不会响应来自客户端的其他请求,因为它只有一个线程来执行你的代码。或者,如果你有某些CPU密集型的操作,比如说,重置图片的尺寸,那也将阻塞所有其他的请求。


...然而,除了你的代码之外,其他的一切都是并发执行


在一个单独的请求里,没有办法可以使得代码并行执行。然而,所有的I/O都是基于时间的并且是异步的,所以接下来的代码将不会阻塞服务器:

c.query(
   'SELECT SLEEP(20);',
   function (err, results, fields) {
     if (err) {
       throw err;
     }
     res.writeHead(200, {'Content-Type': 'text/html'});
     res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
     c.end();
    }
);

如果你在一个请求中这么做,其他请求能够很好得被执行。


为什么这是更好的方式?什么时候我们需要从同步转向异步/并发执行?


采用同步执行是个不错的方式,因为它使得编码变得容易(对比线程而言,并发问题常常让你陷入万劫不复)。

在node.js中,你不需要去担心你的代码在后端会发生。你只需要在你做I/O操作的时候使用回调就可以了。你会得到保证:你的代码不会被中断,并且I/O操作也不会阻塞其他请求(因为没有了那些线程/进程需要花费的开销,比如在Apache中会发生的内存过高等)。

采用异步I/O也很好,因为I/O比那些执行其他操作更昂贵,我们应该做一些更有意义的事情而不是去等待I/O。


一个事件循环指的是——一个实体,它可以处理外部事件并且将它们转化为回调的执行。因此,I/O调用变成了node.js可以从一个请求切换到另外一个请求的“点”,你的代码保存了回调并返回控制权给node.js运行时环境。而回调在最终获得了数据之后被执行。

当然,在node.js内部,仍然是依靠线程和进程来进行数据访问、处理其他任务执行。然而,这些都没有明确地对你的代码暴露出来,所以你不需要额外担心内部如何处理I/O之间的交互。对比Apache的模型,它少去了很多线程以及线程开销,因为对每个连接来讲单独的线程不是必须的。仅仅是当你绝对需要让某个操作并发执行才会需要线程,但即便如此线程也是node.js自己管理的。

除了I/O调用之外,node.js期待所有的请求最好快速返回。比如,那些CPU密集型的工作应该被隔离到另一个进程上去执行(通过与事件交互或者使用像WebWorker一样的抽象)。这很明显意味着当你与事件交互的时候,如果没有另一个线程在后端(node.js运行时),那么你是无法并行化执行代码的。基本上,所有可以emit事件的对象(例如EventEmitter的实例)都支持基于事件的异步交互并且你也可以与“blocking code”交互(例如使用文件、sockets或者在node.js中是EventEmitter的子进程)。使用这种方案的话,就能够很好得利用多核的优势了,可以看看:node-http-proxy。

内部实现

在内部,node.js依赖于libev提供的事件循环,libeio是对于libev的补充,node.js使用池化的线程来提供对于异步I/O的支持。如果你想了解更多细节,你可以看一下libev的文档。


如何在Node.js中实现异步


Tim Caswell在其PPT中描述了整个模式:

  • First-classfunction:例如我们将function作为数据传递,包裹他们以在需要的时候执行。
  • Function组装:就像你了解的关于异步函数或者闭包一样,在触发了I/O事件之后执行。
  • 回调计数器:对于基于事件的回调,你无法保证对于任何特殊的命令,I/O事件都会被执行。所以,一旦你需要多次查询来完成某个处理,通常你仅需要对任何的并发I/O操作进行计数,然后在你确实需要最后的结果的时候检查是否必要的操作都已全部完成(其中的一个例子是在事件回调中,通过对返回的数据库查询进行计数)。查询会被并发执行,并且I/O也对此提供支持(例如可以通过连接池的方式实现并发查询)。
  • 事件循环:上面已经提到过,你可以将blockingcode包裹进一个基于事件的抽象中去(比如通过运行一个子进程,然后当它执行完成之后再返回)。

真的非常简单!

再次申明原文出处:http://blog.mixu.net/2011/02/01/understanding-the-node-js-event-loop/

另外,转载本文请著名“原文出处”,谢谢!