了解Node.js

先看一下Node.js架构图,重点留意下线程池,每一个阻塞(每一个callback)会启动一个线程来处理它(这里的线程池类似点餐的厨房)

Node.js架构图

Node.js应用程序在一个单线程的事件驱动模型中运行,在传统的网络模型中,请求进入一个web服务器,并被分配一个可用的线程。对于该请求的处理工作继续在该线程上进行,直到请求完成后并发出相应。

下面我来介绍一下node.js的事件模型。Node.js不是用多个线程为每个请求执行工作的,相反而是它把所有工作添加到一个事件队列中,然后有一个单独线程,来循环提取队列中的事件。事件循环线程抓取事件队列中最上面的条目,执行它,然后抓取下一个条目。当执行长期运行或有阻塞I/O的代码时,注意这里:它不会被阻塞,会继续提取下一个事件,而对于被阻塞的事件Node.js会从线程池中取出一个线程来运行这个被阻塞的代码,同时把当前事件本身和它的回调事件一同添加到事件队列。

当Node.js事件队列中的所有事件都被执行完成时,Node.js应用程序终止。

NodeJS带来的对系统瓶颈的解决方案:


1、解决并发连接这个问题的:

更改连接到服务器的方式,每个连接发射(emit)一个在NodeJS引擎进程中运行的事件(Event),放进事件队列当中,

而不是为每个连接生成一个新的OS线程(并为其分配一些配套内存)

2、I/O阻塞

NodeJS通过回调函数(Callback)和异步机制会做得很自然


一、单线程:

单线程其实就是按从上到下顺序依次来执行,而且每次只执行一个任务,只有当前这个任务执行完了,才会执行下一个任务。在js引擎中只有一个线程去解析和执行JavaScript代码,即主线程,这就是Nodejs为何是单线程的原因了。但实际上还有其他的线程:处理AJAX请求的线程、处理DOM事件的线程、定时器线程、读写文件的线程等。这些线程称为工作线程。


异步IO:

以下用一个图来描写异步IO



二、事件驱动:

线程驱动是当收到一个请求的时候,将会为该请求开一个新的线程来处理请求。而线程主要是由线程池来管理的。当线程池中有空闲的线程,会从线程池中拿取线程来处理,如果线程池中没有空闲的线程,新来的请求将会进入队列排队,直到线程池中空闲线程

事件驱动编程主要思想是通过事件或状态的变化来进行应用程序的流程控制,一般通过事件监听完成,一旦事件被检测到,则调用相应的回调函数。

事件驱动主要执行过程是当进来的一个新的请求的时候,请求将会被压入队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态

变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数。


下面先来看一个例子:

[javascript]  view plain  copy
  1. console.log("程序开始!");  
  2.   
  3. setTimeout(function () {  
  4.     console.log("执行第一个函数!")  
  5. },0);  
  6.   
  7. setTimeout(function () {  
  8.     console.log("执行第二个函数!")  
  9. },0);  
  10.   
  11. console.log("程序结束!")  
  12.   
  13. /*输出 
  14. 程序开始! 
  15. 程序结束! 
  16. 执行第一个函数! 
  17. 执行第二个函数! 
  18. */  

上面的例子说明了Nodejs是单线程运行、基于事件驱动的,先同步(即按从上到下的顺序)执行整个整个js文件中的代码(此时的事件循环是暂停的),当遇到异步函数(setTimeout计时函数)时,会从线程池中寻求有用的线程来执行该异步函数,当异步函数执行完,就将回调函数放入消息队列里面。当整个js文件执行完后,事件循环开始执行,从消息队列里面取消息,开始执行里面的回调函数。


下面再来看一个例子:

[javascript]  view plain  copy
  1. console.log("程序开始!");  
  2.   
  3. /*模拟计算密集*/  
  4. for(var i = 0; i<1000000000; i++) {}  
  5.   
  6. setTimeout(function () {  
  7.     setTimeout(function(){console.log("执行第二个函数!")},2);  
  8. },0);  
  9.   
  10. setTimeout(function () {  
  11.     console.log("执行第一个函数!")  
  12. },0);  
  13.   
  14. console.log("程序结束!")  
  15.   
  16. /*输出 
  17. 程序开始! 
  18. 大概停顿了一会才输出以下的结果 
  19. 程序结束! 
  20. 执行第一个函数! 
  21. 执行第二个函数! 
  22. */  

该例子只是在上一个例子的基础上加了一个for循环(模拟计算密集);明显发现程序在输出“程序开始!”后停顿了一会才输出下面的结果;这个例子充分说明了Nodejs是不合适用来开发有关计算密集的程序,因为整个javascript代码是运行在单线程中的,除了异步函数会从线程池中寻求可用的新线程浅执行,其他的代码则是在主线程去(同步)执行,当遇到计算密集的情况时,整个线程会被阻塞住,影响到整个程序运行的效率。

三、Nodejs是单线程吗?

首先是这篇非常重要的文章: http://debuggable.com/posts/understanding-node-js:4bd98440-45e4-4a9a-8ef7-0f7ecbdd56cb

我们写下的js代码,是在单线程的环境中执行,但nodejs本身不是单线程的。如果我们在代码中调用了nodejs提供的异步api(如IO等),它们可能是通过底层的c(c++?)模块在另外的线程中完成。但对于我们自己的js代码来说,它们处于单线程中。因为异步函数执行完将结果通过回调函数传给我们的时候,我们的代码一次只能处理一个。

在这里用debuggable.com上的那个文章中的一段比喻来讲,非常容易理解。如下:

我们写的js代码就像是一个国王,而nodejs给国王提供了很多仆人。早上,一个仆人叫醒了国王,问他有什么需要。国王给他一份清单,上面列举了所有需要完成的任务,然后睡回笼觉去了。当国王回去睡觉之后,仆人才离开国王,拿着清单,给其它的仆人一个个布置任务。仆人们各自忙各自的去了,直到完成了自己的任务后,才回来把结果禀告给国王。国王一次只召见一个人,其它的人就在外面排着队等着。国王处理完这个结果后,可能给他布置一个新的任务,或者就直接让他走了,然后再召见下一个人。等所有的结果都处理完了,国王就继续睡觉去了。直接有新的仆人完成任务后过来找他。这就是国王的幸福生活。

这段话对于理解nodejs的运行方式非常重要。

在nodejs中,有一个队列(先进先出),保存着一个个待执行的任务。第一个任务就是我们写的js代码,它最先被执行(相当于国王给第一个仆人任务清单)。在它执行完以后(国王睡回笼觉去了),其它的任务才会加到队列上(相当于第一个仆人按照清单给其它仆人分配任务)。

在我最上面的代码中,我在提交任务时,两次wait,实际上相当于国王在给第一个仆人清单时,突然发呆,仆人只能老老实实地等着,而不会去布置任务。直到国王发了两次呆之后,才去睡觉(我们的代码运行到结尾),这时仆人才敢离开给其他人布置任务。

这就是为什么会先出现两个waited 1xxms,之后才出现任务被执行的信息的原因。

四、process.nextTick

这篇文章也非常重要:http://howtonode.org/understanding-process-next-tick

nodejs的单线程让群中有些朋友很不满,他们认为如果我们需要进行一些密集计算(比如while(true)这样的),岂不是把整个线程等卡死了?我在一些资料上看到,的确是有这个担心,所以nodejs不适合用来开发cpu密集运算的程序,而适合做那些IO操作比较多,但本身不需要计算太多的程序。因为IO操作通过都是通过异步由nodejs在其它线程中完成,所以不会影响到主线程。

但如果我们的程序中,难以避免地需要进行一些密集运算该怎么办?这时需要把计算分解为可递归的步骤,计算一步后,使用process.nextTick将下一步放在队列的最后,让nodejs有机会去处理那些已经在等待的任务。

这里举一个例子,来自前面提到的howtonode上的文章:

var http = require('http');
var wait = function(mils) { 
    var now = new Date; 
    while(new Date - now <= mils); 
};
function compute() { 
    // performs complicated calculations continuously 
    console.log('start computing'); 
    wait(1000); 
    console.log('working for 1s, nexttick'); 
    process.nextTick(compute); 
}
http.createServer(function(req, res) { 
    console.log('new request'); 
     res.writeHead(200, {'Content-Type': 'text/plain'}); 
     res.end('Hello World'); 
}).listen(5000, '127.0.0.1');
compute();

其中compute是一个密集计算的函数,我们把它变为可递归的,每一步需要1秒(使用wait来代替密集运行)。执行完一次后,通过process.nextTick把下一次的执行放在队列的尾部,转而去处理已经处于等待中的客户端请求。这样就可以同时兼顾两种任务,让它们都有机会执行。

不过这种方式对于一个高访问量的网站来说还是不够,因为每步需要1s,这个时间还是太长了。这种情况需要采用其它的方式处理(以我目前刚入门的能力来看还不知道如何解决)。

在群中讨论nextTick时,我们对它的处理产生了分歧。主要原因是由于文中的一句话:

In this model, instead of calling compute() recursively, we use process.nextTick() to delay the execution of compute() till the next tick of the event loop

有的同学认为它是说“把某任务放在当前任务的下一个”,有的认为是放在队列的最尾,争轮不休。最后老雷同志贴上了nodejs的源代码,解决了这个问题:

从这几行代码中,我们可以看出很多信息:

  1. nextTick的确是把某任务放在队列的最后(array.push)
  2. nodejs在执行任务时,会一次性把队列中所有任务都拿出来,依次执行
  3. 如果全部顺利完成,则删除刚才取出的所有任务,等待下一次执行
  4. 如果中途出错,则删除已经完成的任务和出错的任务,等待下次执行
  5. 如果第一个就出错,则throw error

看来有时候找半天资料不如看一眼源代码。

五、注意事项

如前段所讲,我们在js代码中,一定要尽量避免如while(true)这样的循环,或者密集计算。如果一定要这么做,则应该想办法把它分解为可重要执行的小块,通过process.nextTick将它分散开,让所有的任务都有执行的机会。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值