作为从Java转过来的程序员,我一直对nodejs的“单线程、异步非阻塞模型”不是很理解。本想先放着,等熟悉之后慢慢就知道了,没想到今天在学习async的queue
函数的时候卡住了,趁此机会好好学习一下。
卡住我的代码是这样的:
简单解释一下。async的queue是一个任务队列,在本例中设置了3个worker。我通过`pushTask`函数,向队列中放入多个任务。期间等待了两次,分别是100ms和1000ms。等待的目的是想让放入的任务先执行,queue不是异步的吗?
然而执行结果却让我意外:
waited 100ms
waited 1000ms
processing task: t1
running: t1
processing task: t2
running: t2
processing task: t3
running: t3
processing task: t4
running: t4
processing task: t5
running: t5
processing task: t6
running: t6
processing task: t7
running: t7
processing task: t8
running: t8
可以看到,提交的任务都没有立即执行,而是让我白等了1100ms。这是怎么回事?我期望的输出是这样的:
processing task: t1
running: t1
processing task: t2
running: t2
processing task: t3
running: t3
processing task: t4
running: t4
waited 100ms
processing task: t5
running: t5
processing task: t6
running: t6
processing task: t7
running: t7
processing task: t8
running: t8
waited 1000ms
为了解决这个问题,我与群中的几位朋友一起讨论,最终基本弄明白了这个问题,非常感谢他们的帮助。
## 二、Nodejs是单线程吗?
首先是这篇非常重要的文章:[http://debuggable.com/posts/understanding-node-js:4bd98440-45e4-4a9a-8ef7-0f7ecbdd56cb](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`,之后才出现任务被执行的信息的原因。
<div>
## 三、process.nextTick
</div>
这篇文章也非常重要:[http://howtonode.org/understanding-process-next-tick](http://howtonode.org/understanding-process-next-tick "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的源代码,解决了这个问题:
从这几行代码中,我们可以看出很多信息:
array.push
)2. nodejs在执行任务时,会一次性把队列中所有任务都拿出来,依次执行3. 如果全部顺利完成,则删除刚才取出的所有任务,等待下一次执行4. 如果中途出错,则删除已经完成的任务和出错的任务,等待下次执行5. 如果第一个就出错,则throw error看来有时候找半天资料不如看一眼源代码。
如前段所讲,我们在js代码中,一定要尽量避免如while(true)
这样的循环,或者密集计算。如果一定要这么做,则应该想办法把它分解为可重要执行的小块,通过process.nextTick
将它分散开,让所有的任务都有执行的机会。
整个文章讲的的很好,可你的解释还是有点不清楚的地方:
之所以先wait了两次,原因是async其实把那些进queue的任务都process.nextTick()了,就是说放到了下一次event queue的循环里,也就是说现在的任务队列里没有这些任务,也不执行这些任务.等到下次loop才会开始做进queue得任务.