随着Ruby 1.9增加了纤程Fibers(协同程序),以及最近Erlang和Actors的流行,一组少为人之的概念进入了Ruby的编程世界。为了了解Ruby世界中的并发程序,我们采访了Ruby社区的老会员MenTaLguY。他长期致力于Ruby中的并发程序和线程机制,例如fastthread库,通过1.8.x的MRI改进了线程机制。最近他还涉猎了Rubinius。另外他还是JRuby小组的成员。
InfoQ:请介绍一下你的Ruby版Actors库?
MenTaLguY: 实际上,我写了两个Actors库(已经发布)。一个是Omnibus并发库,另一个是Rubinius标准库中的一部分。两者都是Ruby实现的Actors模型的,也就是由Erlang普及的并发模型。并发程序, 从代码并行运行的意义上讲,并不难做到。麻烦出现在当不同的控制线程需要共享同一个资源或者通信路径时。如果你对此不采用一些简单的正规模型来进行结构化,基本上就不可能写出正确的或者最起码有意义的代码,尽管他们表面上看起来能“工作”。
“Actors”就是一个正规模型。一个actor由一个信箱和一个线程组成。它很灵活,actor线程可以等待信箱中出现特定种类的消息,然后“执行”相应的动作,另外也可能再发消息发给别的actor。通过这种自动而显式的消息交换方式,线程可以由一种相对容易理解的方式来通讯。
InfoQ: 它与Ruby的线程体系或者Ruby中新的纤程/协程有什么关系?
MenTaLguY: 我的actor库只是简单地把每个Ruby线程都关联一个信箱,这样每个线程都有了一个actor。但这并不是在Ruby中使用actor的唯一方式。而纤程只是单线程中的协同调度任务,而且你也可以基于actor来实现,就像Tony Arcieri在他的Revactor库中所做的那样。由于纤程比完全线程更轻量级,而且你无需担心抢占问题,因此他的方法很有优势。然而有时你还是需要用完全线程(有时Ruby标准库会迫使你使用它)。
Tony和我曾进行了多次有益的设计讨论;计划让actor的实现尽量有好的兼容性,并提供简单的对象协议,让每个actor实现都可以使用这个协议。从外部看, actors同样是多态的——你最终都会把消息提交给一个信箱。基本上不用关心它是到底一个线程还是一个纤程,或者是什么运行在另一个Ruby虚拟机上的东西。原则上,actor-duck甚至还可以由一些Erlang进程来支持 (例如通过Scott Fleckenstein的Erlectricity).
InfoQ: 我看到你最近对Rubinius库的一些贡献,例如这个对付Actor。Actors是用在Rubinius中吗?
MenTaLguY: 并不是它的一部分(这也是我把它从内核移到标准库中的原因之一)。我认为对它是需求还不到那个程度。
InfoQ: Actors或者用它们实现的信箱有没有可能被用于Evan最近在Rubinius加入的Multi-VM IPC的消息传递中?
MenTaLguY: Actors并没有用于实现MVM IPC机制,但我们想在幕后使用MVM IPC来允许不同VM中的actors互相通讯。
InfoQ: 现在Rubinius线程体系处于什么样的开发状态?都用到了什么——用户线程、内核线程,还是两者的m:n混合?
MenTaLguY: 我们在VM中使用了用户线程,但每个VM都运行在不同的内核线程中。现在,如果你想使用所有的CPU,就要为每个CPU建立一个或两个VM。Evan想要最终在VM中使用m:n模式,但在Ruby中还有许多技术难关需要克服。甚至Ruby 1.9依然在原生线程上裹足不前,所以实际上他们还是用户线程。
可能只有基于支持原生线程运行时(比如XRuby、JRuby、IronRuby)的Ruby实现才完全支持原生线程。如果MVM可以做得更轻量级或者多CPU之间的通讯变得足够复杂(世界正日益变得NUMA化),原生线程就不会变得那么重要了。
但是,有一种情况无法摆脱原生线程:使用那些不支持异步操作的设计糟糕的IO API。在这种情况下,你需要使用多原生线程,而不是使用多核。有时你还不得不选择一个专门的进程来等待阻塞调用来结束这些,你的其它代码也随之结束。
但愿将来我们能少看到这种API。Tony的Revactor库带来了一线希望:他用actor来对付IO,因此你的代码执行可以被IO事件驱动,而不是干等着阻塞调用的到来,或者承受控制的变换而变成了巨大的状态机。现在,Revactor库还用在MRI 1.9上,但我们有望移植它,或者在Ruby中实现一个类似的库。
InfoQ: Rubinius好像有了很丰富的并发概念和工具——线程、actor、多虚拟机+消息传递IPC等等。
MenTaLguY: 从历史上看,在这一点上,并发是一个非常重要的东西,我想是受了Rubinius的影响。
InfoQ: 我注意到其中的一个工具是通道——它在Rubinius中扮演什么角色?(我注意到快速调试器使用通道来通知调试器线程等)
MenTaLguY: 通道是Rubinius中的基本通讯方式;其它所有方式都基于它。基本的并发模型差不多就是异步的pi演算去掉同步和一些诸如非确定性选择之类的公用扩展(需要仲裁通道操作)。我提倡使用pi演算通道是由于它的简单性,换句话说是由于它的高效性和可维护性。
现在,pi演算已经可以很好地直接用于“局部的”(VM内的)地方,但还不太好用于实现分布式的地方,因为在pi演算中,通道两端都是可活动的。由于写操作是异步的,你可以随心所欲地写。但对通道的读操作是同步的。当一个通道遇到多个读操作时,这些读操作必须集中进行。当这些操作互相距离遥远时,就很不妙。
这就是我对actor如何应对大规模情况感兴趣的一个原因。Actor信箱的异步计算特性与通道有点相像,除了只有(异步的)写操作一端是单独可活动的;读操作一端被紧密绑定在一个特定的本地代理上,且不需要考虑“远距离”协调的问题。
InfoQ: Ruby 1.9增加了纤程和协程——它们是如何在Rubinius中实现的?
MenTaLguY: 我认为纤程在有了Rubinius Tasks之后实现起来并不麻烦;纤程和Tasks其实很相似。
InfoQ: 你对纤程和协程有什么见解和意见吗?你会使用他们吗——用来做什么呢?
MenTaLguY: 我认为他们可以很好地代替状态机,特别是协程令你更自由地使用库代码。不过,当状态机足够小或者在一些情况下,比如适合用Ragel之类地东西来产生它时,状态机仍是更好地选择。
InfoQ: 你现在拥有对JRuby的贡献权对吗?你对JRuby的兴趣在哪?或者说你致力于它的哪些方面?
MenTaLguY: 是的。我的主要兴趣是并发程序:Ruby和原生线程的结合提出了一些有意思的挑战。因此我一直在修正并发程序的错误,并确定我们应该为并发程序提供怎样的保证。有可能的话我想最终把Java的并发程序的便利性引入Ruby,希望通过移植的方式来进行(这也是Omnibus Concurrency库的一部分任务)。
InfoQ: 你还参与了其他什么项目吗?
MenTaLguY:除了偶尔对Shoes打些补丁外,我从事一些未发布库的工作,其中大部分将会在准备好的时候发行和公布。这里可以讲讲我最近发行的一个库,尽管还没有正式公布:“case”gem包。 它可以令Ruby的case-match运算支持数组、结构体和任意谓语的模式匹配。
require 'rubygems'
require 'case'
Foo = Case::Struct.new :a, :b
def example(arg)
case arg
when Foo[:blarg, Object] # matches any Foo with .a == :blarg
# ...
when Foo[10, 20] # matches only a Foo with .a == 10 and .b == 20
# ...
when Foo # matches any Foo
# ...
when Case::Any[String, Array] # matches either a String or Array
# ...
# matches a three-element array with initial elements 1, 2:
when Case[1, 2, Object]
# ...
# matches any Integer > 10:
when Case::All[Integer, Case.guard { |n| n > 10 }]
# ...
end
end
Tony和我在我们的actor库中使用case-match运算符(===)来选择特定种类的消息去等待。因此。这个gem包在这里很有用。
欲详细了解MenTaLguY,请访问他的博客http://moonbase.rydia.net/或者关注他所参与的项目。要详细了解Actors,请阅读最近对Tony Arcieri的采访,他是Revactor的开发者。Revactor是一个为高性能网络应用开发的应用程序框架。它面向Ruby 1.9,并使用了诸如纤程的并发特性。关于Rubinius的详情,请参见InfoQ的Rubinius专题内容。
查看英文原文:Ruby Concurrency, Actors, and Rubinius - Interview with MenTaLguY