Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2012-05-17) Nodejs的单线程与异步的初步理解

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

作为从Java转过来的程序员,我一直对nodejs的“单线程、异步非阻塞模型”不是很理解。本想先放着,等熟悉之后慢慢就知道了,没想到今天在学习async的queue函数的时候卡住了,趁此机会好好学习一下。

一、卡住我的代码

卡住我的代码是这样的:

var async = require('async'); var pushTask = function(name) { q.push(name, function(cb) { console.log('running: ' + name); }, function(err){ console.log('finished: ' + name); }); } var wait = function(mils) { var now = new Date; while(new Date - now <= mils) ; } var q = async.queue(function(name, task, callback) { console.log('processing task: ' + name); task(callback); }, 3); pushTask('t1'); pushTask('t2'); pushTask('t3'); pushTask('t4'); wait(100); console.log('waited 100ms'); pushTask('t5'); pushTask('t6'); pushTask('t7'); pushTask('t8'); wait(10000); console.log('waited 1000ms');
简单解释一下。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的源代码,解决了这个问题:

image

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

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

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

四、注意事项

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

一些评论

squallssck

整个文章讲的的很好,可你的解释还是有点不清楚的地方:

之所以先wait了两次,原因是async其实把那些进queue的任务都process.nextTick()了,就是说放到了下一次event queue的循环里,也就是说现在的任务队列里没有这些任务,也不执行这些任务.等到下次loop才会开始做进queue得任务.

comments powered by Disqus