node.js和coroutine

很久之前,我就琢磨着要用js来写服务器端的东西,那时候只有netscape有整套的基于js的解决方案,当然那是要钱的。我用了asp+jscript做了一个wiki,但是asp毕竟不是开源的东西,又局限于windows平台,没有足够的组件可用,后来放弃了这条路子。然后曾经想自己来用Firefox的Spidermonkey的js引擎来写一个服务器端的js平台(其实确实也有一些),但一直没能够闲下来好好研究这个问题。而且那时候根据很多测试,Spidermonkey的性能和其他语言相比十分一般,也就和php差不多。

再后来Google推出了Chrome,其js引擎v8,把js的速度推上了一个新的层次,我当时就认为,v8可以用来搞服务器端脚本。然后就听说基于v8的node.js,而且听说node.js是针对高并发的。我满心欢喜,前几天终于有空研究一下node.js了。

node.js强调异步操作,使用事件进行驱动,这把客户端的编程模型放到了服务器端。我觉得强调异步操作是个很大的进步,因为他让程序员知道分布式编程中的一些陷阱

我本以为node.js是支持Coroutine的,但却没有。没有Coroutine,纯粹使用事件和回调函数,使用CPS风格来进行编程,我觉得会非常累。没有coroutine的支持,我认为node.js的发展会遇到一定的瓶颈。

很多评论都会拿erlang和node进行比较,但有支持node.js的人认为node.js有他自己的风格和目标,不需要coroutine。不过讨论的基本共识是,真正要开发高并发的话,还是用erlang更为合适,他其概念模型也更清晰,而且并发性能更好,原生支持分布式和容错。但erlang语言本身对于js来说,确实难以上手。

不过,上手容易对于node.js来说,已经是非常了不得的优势了。

因为我想到了php。当时php流行起来的时候,性能不行,语言设计也很糟糕,各种问题,但他还是流行起来了。原因就在于php上手容易,你可以quick and dirty地写出很多东西。其实如果要写一些严肃的东西,很多人还是会考虑使用J2EE的一套东西。

同样的情况现在可能会发生在node.js身上,因为node.js上手容易,quick and dirty就写出了很多需要高并发的东西。

至于什么v8的计算性能要优于python和ruby,这并不是关键。当年php也没比python perl快,当然现在也没python和perl快。现在v8很快,但也没比Lua快,更没有LuaJIT快。

当然,Lua上手也很简单,但为何Lua没有流行?其实Ruby和Python上手也不难,为什么没有流行?

这让我又想到了“工业强度”这个概念。一个工具能有多少工业强度,他就有多流行。以前也有很多人讨论过这个概念,当然我的思维方式和他们都不一样。我觉得一个工具的工业强度,可能就有以下概念

  1. 能用这个工具的人是不是好找,是不是容易培养
  2. 这个工具有没有足够多的应用来证明他是可行的
  3. 这个工具配套的工具是不是完善
  4. 小白和牛人是不是能在这个工具上协作好。

细分领域的事情我就不管了。我觉得以上几点,就可以决定某个工具是不是能流行起来。

比如现在真正意义上的工业编程语言当之无愧是Java、C/C++。为何他们能成为最流行的,我觉得首先是第一点。为什么第一点很重要,因为对于公司来说,它肯定不希望自己被绑在某几个程序员身上,也希望能通过多招人来提高生产力。像会Java C++的人,都好像是从流水线上下来的一样多,我们国家的各种大学的计算机系和一些专门的培训机构可以批量生产这种人。本身行业对这种人员的需求量很大,但供应量也很大。而且当有这么多人之后,各种资料也会多起来,对于深入研究也是很方便的。另一方面,像Erlang Haskell这种函数式编程语言,并不好理解,其实我个人都没法理解Haskell的manod,更不要说初级程序员了。如果对于某种编程语言的程序员的培训周期很长,它注定在工业强度上不及Java和C/C++。当然其实Java C/C++也相当复杂,比如C++的模板什么的,内存管理等等,但实际写出实际可用的程序并不太难,这些并不一定需要接触到。

JavaScript由于占据浏览器的统治地位,所以有很广泛的受众,就这点上来说,node已经很占优势了。

对于第二点,像Lua,上手也很简单,性能也很好,为什么不流行呢?Lua被人广泛了解,是通过WOW的插件体系。所以现在Lua成为游戏开发中非常常用的工具。所以,一个工具要能流行,还需要成功的应用来支撑它,但往往会让他局限在这个成功应用的范围中。当然,Lua知名度不高,其实好的Lua程序员并不多,招人困难,普通公司并不会拿他来做主要开发工具。

Ruby也是借助Rails一下就流行了起来,Rails是37 Signals为开发他们自己的产品出现的框架,加之twitter也应用了Rails,让Ruby一下在Web开发领域占据了一席之地——不过性能问题也成为很大的诟病。

JavaScript的成功应用不必说,太多的AJAX网站,但是node.js在服务器端的成功案例还比较少。所以node.js自己本身还不是很流行。

第三点,当年ASP很流行,就和PHP一样,他上手容易,但是后来没落了,原因就是在于,ASP的配套工具太少了,ASP被局限于微软的平台之上,微软改向了ASP.NET,ASP就没落了。对于ASP,只能运行在IIS上,IIS本身可扩展的功能就少,ASP只能使用Access,Sql Server这些通过控件的数据库。而PHP,可以运行在windows、linux上,可以和很多组件配合,相当丰富的扩展,以及很多他人编写的代码库。

Perl有CPAN,Python有PIP,Ruby有RubyGems,现在node.js也有了NPM,这个非常好。

对于第四点。这其实就是我关心为何node.js需要coroutine。

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

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

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

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

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

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


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

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

这个是node.js的代码

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

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

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

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

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

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

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

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

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


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

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

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


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

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

“node.js和coroutine”的12个回复

  1. node的作者ry也有考虑过Coroutine,但他认为如果加进这个模式那开发者还要去关心协程安全的问题。详见:http://nodejs.org/jsconf2010.pdf

    我觉得Coroutine不流行也是有原因的,ruby,python的实现中都包含了,但社区的人们还是花时间去实现EventMachine,Twisted。Coroutine不止内存占用大,而且切换效率也是一个问题,另外在遇到IO的情况,处理起来和多线程一样麻烦。

  2. @kaichen
    这个pdf我看过,我记得有位大师说过,好的工具就好像一把锋利的刀,使用得当能产生很高的效率,使用不当就会有危险,但是制造刀的人没必要处处考虑为了使用刀的安全,这是过度设计。比如C,用C的人就该知道如何处理指针,否则他干嘛要用C呢。但就Node.js的目的而言,Coroutine是必须实现的,node.js是为了简化高并发应用的开发,而不是让他更复杂,Coroutine和事件+回调并不矛盾。

    首先,运行效率不是关键问题,开发效率才是关键问题,否则干嘛那么多人用php呢?更不要说Rails了。真正要效率的话,可能大家还会考虑C/C++/Java。

    即使加入了Coroutine,原来并发1w的程序现在只能并发8k了,但是更高编程效率我觉得更值。况且并不一定会降低多少运行效率。

    第二,Ruby1.9之后才有语言直接支持的Coroutine(他们叫Fiber),EventMachine是在1.8的时候就有了。而Ruby社区也在EM的底层上出了一个EM-Synchrony的包,用来简化回调函数的编程。

    Python语言本身只支持Generator,不是完整的Coroutine,有些库为Python添加了Coroutine支持,比如Greenlet,所以Python才会有twisted。Stackless Python是本身对Python解释器进行了修改来支持Coroutine,叫Micro-thread。

    第三,你不能说Coroutine不流行。Python中也有很多使用Coroutine的框架,底层使用event loop,比如Gevent.

    大部分的脚本语言都有一个GIL的问题,他们没法通过单一进程实现真正的多核上的并行,所以大家都在把目光集中到事件机制和Coroutine上。(Google的一帮子人想从Python从把GIL移出结果失败了)

    其他很多语言中都有coroutine,比如最近挺流行的一种语言Scala,也是通过Actor模型来实现高并发,Actor本身就是一种coroutine。

    第四,完全事件+回调函数的性能未必就比使用Coroutine的好,我本人没有做过性能测试,不过你可以试试,以下给你几个参考

    http://nichol.as/asynchronous-servers-in-python

    纯粹是事件和回调机制,Tornado的性能要优于Twisted,而Coroutine和event-loop结合的Gevent并不比Tornado差。

    而且如果是Stackless Python的话,内存占用和切换效率更好。

  3. 我在Lua讨论组才发了一封信,咨询关于为何Lua不像Node.js这样火,Lua是否可以移植node.js。(我个人没有server端编程经验,很难问的比较到位)

    对于Lua而言,它支持协程,支持服务器端编程(比如使用libevent之类),但是为何应用如此少?不知ShiningRay怎么看?

  4. 好文章,有理有据。在软件编写上面,性能与逻辑其实是一对矛盾。
    在性能上,同步<异步,但在逻辑上不太好理解。
    因此,协程可以在异步的基础上实现一个伪同步,稍微浪费一点性能。
    相比真同步,性能上与逻辑取得平衡。

  5. 在coffeescript加持之下,代码如下。
    fs.readFile ‘a.txt’,(err, data) ->
    … if err
    fs.writeFile ‘b.txt’, data, (err)->

    个人觉得能不复杂化就不要’更’复杂化吧。

  6. 我觉得node.js现在的编程效率刚刚好,编程效率/自身性能恰好在一个平衡点,加入coroutine这个平衡点会打破

  7. 用 Node.js 写了一些东西,
    越写越感觉蛋疼。
    回调来回调去,
    逻辑都被打断了。

    计划改版,
    用 Node.js 接受高并发的连接,
    然后将任务转给 Python 线性的慢慢处理。

  8. 我觉得回调函数没什么不好的,直观而且天然,反而是 “协程” 这种概念,属于“结构化编程”了。

    另:汇编里面,回调是天然的,lisp里面也是。

发表评论

电子邮件地址不会被公开。 必填项已用*标注