为何node.js需要coroutine【转】

转自 http://shiningray.cn/node-js-coroutine.html

这篇文字以前看到时候印象不错,但是没有体会,最近重新阅读,觉得非常棒,转一下。

=================

这其实就是我关心为何node.js需要coroutine。

很多语言,像Java C/C++,虽然要深入了解他们,很复杂,但一个应用可以由资深的程序员,写出一个框架,然后通过框架隐藏那些复杂的细节,然后由其他初级程序员来编程实际的复杂应用逻辑。现在很多外包公司就是这样,甚至是由主程序员写一个代码模板,然后让其他小弟们来改改。

Python、Ruby就在这上面更进一步,不仅可以开发一个框架,还能设计一种DSL,让逻辑的编写更加简单。

但是node.js虽然隐藏了event-loop和async io的细节,但是却把异步处理的流程控制的问题丢给了开发人员。即使是上层负责逻辑的程序员,也常常被异步所干扰。

计算机本身其实多个不同的设备之间进行通信,必须要考虑很多同步的问题。冯诺依曼机的理念就是把一些同步发生的东西,通过时钟进行同步,让本来必须考虑并行的编程,简化为了串行编程,然后我们就可以使用简单的流程图了。这是冯诺依曼机最大的贡献。

很多时候,对于编程中的某个连续的逻辑来说,我其实并不关心读取文件是阻塞还是异步,请求数据库要考虑超时什么的。我的目的其实很简单,就是读取文件,获得内容,然后放入数据库等等。

而现在node.js的做法似乎就是抛弃了这种模式,但是node.js却无法实现真正的并行编程,比如利用多核,这是很奇怪的事情。


然而如果有了coroutine,那么我们可以设计出一个框架,在框架之上,普通程序员还可以继续按照以前的方式来写代码,像底层的异步操作则应该是资深程序员关心的事情。

比如,一个从a文件读取内容,然后写入b文件的一个代码:

这个是node.js的代码

fs.readFile('a.txt', function(err, data) {
  if(err){throw err;}
  fs.writeFile('b.txt', data, function(err){
    if(err){throw err;}
  });
});

而传统方式的伪代码如下:

try{
var data = fs.readFile('a.txt');
fs.writeFile('b.txt', data);
}catch(err){}

很明显是传统方式的伪代码更加清晰,node.js使用的CPS给人感觉非常冗长。如果有coroutine,那么node.js还可以进一步把核心库中的*Sync版本给删减。

那么有了Coroutine的话,我们可以给原先的代码改成这样

function readFileSync(file){
   var co=Coroutine.current();
   fs.readFile(file, function(err, data){
     co.resume(data); //1
   });
   return Coroutine.yield(); //2
}
var data = readFileSync('a.txt')

这段代码使用当前的coroutine来等待io操作,会在(2)处挂起当前coroutine,等待唤醒,当异步i/o执行完成以后,则在回调函数的(1)处唤醒当前的协程。

当然这样写的话会阻塞当前的协程,如果要不阻塞当前的协程,我们可以这样写:

var co = Coroutine.create(function(){
   var data=readFileSync('a.txt');
   writeFileSync('b.txt', data);
});
co.resume();

这样创建一个新的协程把任务单独隔离开来,至于你什么时候需要调用该协程,则由自己安排。

这样使用Coroutine的好处是,你可以对自己的每一个请求进行概念抽象,把每个请求封装成coroutine,那么在这种请求中的处理逻辑还是可以按照原来的方式去编写。

有些人可能认为coroutine太浪费内存,但据目前很多coroutine在高并发程序中的应用来看,是可以接受的;而回调模型要引入大量的函数对象以及大量闭包,未必就能更省内存。

另外还有人提到Coroutine也可能会出现一些同步问题,但这并不能成为不使用Coroutine的理由,写的不好的回调机制一样也会产生同步问题。


实际的应用可能更加复杂,举个例子。在糗事百科的web端,常常要先从memcached中获取缓存,如果缓存不存在,则读取数据库,然后再把内容存入memcached中。这是个很常用的逻辑。来段同步版伪代码

var data = Memcache.get('key');
if(!data){
  data = Mysql.execute(sql);
  Memcache.set('key', data);
}
//后面的代码

当然这里也不考虑什么竞争条件什么的问题了。
如果是node.js,那么则变成了

function rest(data){
  //后面的代码
}
Memcache.get('key', function(err, data){
   if(data){
     rest(data);
   }else{
     Mysql.execute(sql, function(err, data){
        Memcache.set('key', data);
        rest(data);
     });
   }
});

本来很清晰的代码,却被回调函数弄得支离破碎,即使使用一些像Step/Do这些DSL来协助,还是不如原来的更加直观。


综上所述,node.js的特点是易于上手,容易编写,同时性能还不差,但问题在于,到了一定复杂程度之后,代码编写就比较困难了,不容易构建较复杂的应用,所以引入Coroutine才可以让node.js真正进入大型应用的领域。

PS,其实有call/cc也可以解决很多问题。

发表评论