Node.js的第一个基本论点是I/O开销很大。
当前编程技术中等待I/O完成会浪费大量的时间。有几种方法可以处理这种性能上的影响:
- 同步:每次处理一个请求,依次处理。优点:简单;缺点:任何一个请求都可以阻塞所有其他的请求。
- Fork一个新进程:开一个新进程来处理每个请求。优点:容易;缺点:不能很好的扩展,成百上千个连接意味着成百上千个进程。fork()函数相当于Unix程序员的锤子,因为它很有用,每个问题看起来就像一个钉子,通常会被过度使用。(译者注:直译比较拗口,我理解的意思是,Unix程序员可以用fork()函数解决很多问题)
- 线程:开一个新线程来处理每个请求。优点:容易,与使用fork相比,对内核更友好,因为线程通常开销较小;缺点:机器可能没有线程,线程编程可以非常复杂非常快,但是必须关注共享资源的权限问题。
第二个基本论点是每个连接开一个线程内存开销很大。
Apache是多线程的:它会为每个请求生成一个线程(或者进程,依据配置文件conf中的配置)。随着并发连接数的增加,你可以看到基本开销如何耗尽内存,需要更多的线程来服务同时在线的客户端。Nginx和Node.js不是多线程的,因为线程和进程会带来很大内存负担。它们是单线程,但是基于事件的。在一个线程中处理多个连接的开销比在成千上万个线程或进程中处理连接的开销小。
Node.js让你的代码保持单线程……
它真的是单个线程在运行:你不能有任何并行的代码操作;例如模拟一个"sleep"阻塞服务器一秒钟:
var now = new Date().getTime(); while(new Date().getTime() < now + 1000){}
当这段代码执行时,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(); } );
如果你在一个请求中这么做的话,当数据库正在执行sleep时,其他的请求也可以被处理的很好。
为什么这是有好处的?什么时候我们可以从同步变成异步/并行执行?
同步执行有好处,因为可以简单的写代码(与多线程相比,并发性问题有导致WTFs的趋势)(译者注:WTF在此处是What The Fuck的缩写)。
在node.js中,你不应该担心后台发生了什么:当你做I/O操作时,只需要使用回调函数;可以保证你的代码不会被中断,I/O操作不会阻塞其他请求,也无需承担每个请求的线程/进程成本(例如Apache中的内存开销)。
异步I/O操作也有好处,因为I/O操作比大多数代码开销更大,我们应该做一些更好的事情而不仅仅是等待I/O操作。
事件轮询是"一种掌握和处理外部事件并且把它们转成回调调用的实体"。因此I/O调用是一些调用点,node.js可以在这些点从一个请求转换到另一个请求。在一次I/O调用时,你的代码保存回调并且把流程控制权返给node.js的运行环境。当数据可用时,回调方法会被调用。
当然,在后台还是有线程和进程进行数据库访问和流程执行的。但是你的代码不需要显示的接触这些,因此你不必担心它们,只需知道I/O交互就行了。例如从每个请求的角度来看,数据库或其他流程需要异步,因为这些线程的结果通过事件轮询返回给你的代码。与Apache模块相比,因为不需要为每个连接创建线程,可以减少线程和线程开销;只有当你确定有些事情需要并行处理,即使这样也通过node.js来处理这种管理。
除了I/O调用,Node.js希望所有的请求都可以快速的返回;例如CPU密集型任务应该被分离到另一个进程中,然后通过事件进行交互,或者通过使用一个抽象概念像WebWorkers。这(明显的)意味着你不能并行化你的代码,除非后台有另一个线程,你可以通过事件与之交互。基本上所有的能发出事件的对象(例如EventEmitter的实例)都支持异步事件交互,你可以用这种方式与阻塞代码交互,例如正在使用的文件,sockets或者子进程,所有这些都是node.js中的EventEmitters。多核可以使用这种方法;参见node-http-proxy。
内部实现
在内部,node.js依赖libev提供事件轮询,libev依赖libeio,libeio使用线程池来提供异步I/O。想要进一步学习事件,看下libev的文档。
那么在node.js中如何做到异步?
- 一等函数。例如我们把函数作为数据传递,改变它们,需要的时候会执行它们。
- 复合函数。又称为匿名函数或闭包,在基于事件的I/O中发生某种事件时执行。
(译者注:即第一种是传递定义好的函数,第二种是传递匿名函数)
原文链接http://blog.mixu.net/2011/02/01/understanding-the-node-js-event-loop/