很久以前翻译的,忘了出处(:з」∠)
首先需要知道的是,node.js的 I/O是异常昂贵的
The cost of I/O
L1-chache 3 cycles
L2-cache 14 cycles
RAM 250 cycles
Disk 41000000 cycles
Network 240000000 cycles
所以一旦当前的编程技术是通过等待I/O完成的话,那将是非常浪费的一件事情,现在有几种方法可以处理对于性能的影响(可以参看异步套接字编程)
- 同步: 依次处理每个请求,并且每次只处理一个请求. 优点: 非常的简单,缺点: 一个请求就足以阻塞出整个应用
- 创建一个新的进程(fork a new process): 创建一个新的进程来处理新的请求. 优点: 也很简单,缺点: 多少个连接就意味着多少个进程,fork()是Unix程序员的锤子,因为所有问题看上就像个钉子,不过它通常都是多余的力量(overkill)
- 线程: 启动一个新线程来处理请求. 优点: 也很简单,比起让内核(kernel)使用fork()来说要更加温柔些,毕竟线程通常就有很多,而且资源开销更小,缺点: 机器有可能并没有好的线程程序,导致可能非常的复杂或降低速度,附送的还有对于共享资源的访问也可能出现问题
其次,第二个问题是,线程共享的链接缓冲池,也是非常昂贵的
Apache 是多线程的,
每个请求都会生成一个线程或进程,它取决于conf,你可以看到这玩意是怎么吃内存的,而且随着并发连接数量的增多,所需要的线程也就越多,比如什么Nginx,节点等
而js并不是多线程,线程和进程会带来沉重的内存成本,所以它是基于事件的单线程,通过这样来消除成千上万的线程/进程,并且将所有需要处理的问题都绑定在一个线程上
Node.js 始终保持着单线程的优良传统
而它毫无疑问也是一个真正的单线程 : 在这里,你无法执行任何的并行代码,比如做一个延迟(sleep)来阻止服务器一秒:
while(new Date().getTime() < now + 1000) {
// do nothing
}
而在这期间,js不会回应任何来自客户的请求,因为它只有一个线程执行的代码,或者如果你会一些别的什么cpu什么的代码,说: 给我搞搞图片大小,就算这样,js依然会阻止这些请求
不过,不管什么都好,都是离不开并行的,尤其是你的代码
代码是没办法并行的运行在单个请求上的,不过,所幸所有的I/O都是一个事件并且异步的,所以下面这种方法并不会阻塞服务器
c.query(
'SELECT SLEEP(20);',
function (err, results, fields) {
if (err) {
throw err;
}
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('Hello Return from async DB query
');
c.end();
}
);
如果你在一个请求中执行了该操作,那么在这个数据库处于阻塞(sleep)状态的时候,你依然可以处理其他请求
这种方法可以很好的解决当你需要从同步走向异步/并发执行时的问题
具有同步执行时好的,因为它简化了代码(相对于并发问题,它可能会倾向于WTFS线程问题)
(Having synchronous execution is good, because it simplifies writing code (compared to threads, where concurrency issues have a tendency to result in WTFs).)
在node.js中,当你正在处理I/O的时候,不应该担心后台的问题,解决好你的回调问题,还有保证你的代码不会被打断,因为I/O并不会阻止其他请求,所以也不必承担线程/进程的请求成本(比方说在Apache中的内存开销)
异步I/O是非常不错的,因为I/O比大多数的代码开销更大,更昂贵,所以我们应该在等待I/O(阻塞过程)的过程中做更多的事情
(这个机制很大程度上是源于js本身就是一个非阻塞I/O)
事件轮询(eventloop)是"一个解决和处理外部事件时将它们转换为回调函数的调用的实体(entity)",所以当代码调用一个I/O的时候,node.js可以从这个请求切换到另一个请求,当调用I/O的时候,代码将会保存回调并且返回某些结果给node.js运行环境中,只有当数据实际可用的时候(或者说不那么阻塞了(sleep事件过了等)),回调便会被调用
当然,在后台中,会有来自DB的线程和进程及其他进程在访问,然而,这些都没有明确的暴露给你的代码,所以你大可以不必担心来自其他I/O的干扰或相互作用,比方说,并不需要在意像数据库或者别的异步进程这些,因为从请求的角度看来,这些线程的结构都是通过事件轮询返回到你的代码中的,相比起Apache模型,少了许多线程和连接的开销,因为线程不需要遍历每个连接,只有当你必须要使用其他的并行操作等,哪怕这服务器管理是node.js,也阻止不了你
除了I/O调用外,node.js希望所有的请求都能迅速返回,比如说像当cpu快速密集处理一堆请求后,应尽快分离掉这些进程,你可以与事件或是通过使用一个抽象的玩意比如WebWorkers互动,这很显然意味着你无法将你的代码通过事件脱离一个后台的线程独自并行.基本上所有的对象发出事件(比如EventEmitter的实例)都是支持异步事件的互动的,你可以使用这种方式,比如使用文件阻塞代码交互,sockets或是子进程等,这些都是可以在node.js中与事件进行互动的,多核的机子可以使用这些方法,(可以查阅关于Http与Node)
内部实现
在内部,node.js依赖于测试程序提供的时间循环,并辅以libeio采用混合线程来提供异步I/O,想知道更多的话,可以查阅下libev documentation
js内部同样有事件轮询机制
那么我们要怎样在node.js中使用异步?
Tim Caswell描述了这么个模式在这( 网址失效了)
- 第一类函数(First-class-functions).我们将函数作为参数传出去,在需要的时候执行他们就可以了
- 复合函数(Function composition).也就是所谓的匿名函数或者是当I/O这些事件执行后回调的函数
请求会将所有的事件都放入到队列中,而node.js会不断询问是否有事件,如果存在事件那么便会立即执行,如果执行过程中存在阻塞事件的话,那么node.js会将它放到一个专门的线程池中专门执行这些操作