Ruby 1.9 加入纤程实现轻量级并发

  • Werner Schuster
  • 仝键

2007 年 9 月 5 日

话题:Ruby编程语言语言 & 开发架构

Ruby 的线程成为人们的讨论议题已经很长时间了。未来版本的 Ruby(1.9 及更高)是否将使用内核线程(kernel thread)替代用户线程(userspace threads),仍有待决定。最近,关于 Ruby 中这一系列问题的另一条途径已经出现。David Flanagan 指出了Ruby 1.9 分支中的一个新特性——纤程(Fiber)

下面是如何使用新的 Fiber 类(注意:类名可能会改变)生成(generate)一个 Fibonacci 数的无穷序列的范例。我使用“生成(generate)”这个词采用的是在 Python 中生成器(generator)的意义上的含义。Ruby 中新的纤程是半协程(semi- coroutines)。
1  fib = Fiber.new do

2 x, y = 0, 1

3 loop do

4 Fiber.yield y

5 x, y = y, x + y

6 end

7 end

8 20.times { puts fib.resume }

这段代码会打印出前 20 个 Fibonacci 数。它使用了一个叫做协程(Coroutine)的概念。基本上,调用Fiber.yield方法会停止(“挂起”)这段代码的执行(请不要把它和用于执行 block 的yield关键字相混淆)。如果你熟悉调试器,那么请想象一下对某个线程点击“挂起”按钮或者看这个线程因为断点命中而中断。fib是这个纤程的句柄,你可以用它对纤程进行操作。在第 8 行出现的fib.resume所完成的正是该代码所表达的意思:它恢复了Fiber.yield调用之后语句的执行,使该纤程恢复执行状态。

第 4 行显示了Fiber.yield带着一个参数y被调用。在某种程度上,这可以被认为是与return y相类似。子程序(subroutine)的return和协程的Fiber.yield之间的区别在于代码调用后其上下文发生的变化上。这就是说,return的意思是一个函数调用的激活格局(或者叫做栈格局)被取消分配了(deallocate),即所有的本地变量都消失了。在一个协程里,yield保存着这个激活格局并且其内部的所有数据还是存在的,因此在调用了resume方法之后,代码还能继续使用这些环境。

现在这段代码如何工作就变得很清晰了:它只是一个循环——通过相继迭代的方式计算出 Fibonacci 熟。一旦完成一次计算,它就将自己挂起,并将 CPU 的控制权让给别人。当代码性药序列中的下一个数时,它只需要恢复 Fibonacci 代码,代码运行下一次迭代,然后通过调用Fiber.yield将自己挂起让出控制权(以及下一个 Fibonacci 数)。

这是在 Ruby 1.9 分支中加入的一个相当新的特性,而看起来它的细节仍未确定。纤程这个术语对于 Windows 程序员来说,可能并不陌生。MSDN 中是这么解释的

纤程是一个必须由应用程序手动调度的执行单位。纤程运行在调度线程的上下文中。每一个线程可以调度多个纤程。一般来说,纤程所能带来的优势是不及一个设计优良的多线程应用的。但是,对于被设计成必须调度自身线程的应用程序来说,使用纤程可以让它们的移植变得更为容易。

Ruby 1.9 VM(以前被称作YARV)的开发者笹田耕一(Sasada Koichi)就此在 ruby-core 邮件列表中给出了更详细的一些信息

这些方法名(resume/yield)来自 Lua。“transfer”来自 Modula-2,“double resume error”来自 Python 生成器。顺便说一句,我正在考虑使用“纤程(Fiber)”这个名字。目前“纤程”的意思等同于“半协程(Semi-Coroutine)”。Fiber::Core 就是协程。是的,“纤程”这个名字是来自于微软,但是它的意义等价于半协程,如 Lua 的协程和 Python 的生成器。

半协程不对称(asymmetric)协程,在它们对控制权传递的选择上收到限制。不对称协程只能将控制权传递它们的调用者,而对于协程来说,只要它们有相应的句柄,它们就可以自由地将控制传递给其它的任何一个纤程。

前面的例子展现了一个半协程是如何被当成一个生成器来用的。比如,便利地生成 Fibonacci 数。某些语言,比如 Python,在语言中支持生成器且有特定的语法支持。从引文中看来,似乎半协程(纤程)和协程(Fiber::Core)行为都是支持的。最终 Ruby 1.9 及以后的版本中会出现什么以及它会如何命名尚待分晓,但是 Ruby 语言的创造者松本行弘(Matsumoto, Yukihiro)认为他们安全:

在核心开发人员之间仍然有热烈的讨论,但是纤程(和外部迭代器)比起 Continuation,更可能留在最终的 1.9 中。

注:Continuation,一个长期在 Ruby 1.9 分支中缺席的特性。尽管对于它是否可以使用 Ruby 1.9 的核心线程实现,人们尚存疑虑,但在五月份它仍被加到 Ruby 1.9 中

除了实现了控制结构,协程提供了一种使用轻量级并发(lightweight concurrency)的方式。实际上它们允许使用协同调度来实现用户线程。协程既能将控制权让给彼此,也能通过将控制权交给调度协程,让其决定下一个调度的协程这种方式来实现集中式调度。

这样,关于 Ruby 1.9 转向更为重量级的内核线程的疑虑,就迎刃而解了。Ruby 1.8 线程被构建成一个用户线程系统,这样做的好处是线程管理的开销更少。创建一个内核线程要进行对操作系统的系统调用,比起对线程系统的进程内调用要费时得多。例如,JRuby 使用内核线程,但也在尝试使用线程池来弥补创建开销的性能不足。

然而,创建大量的内核线程仍旧有许多性能开销,或者会直接导致对线程有硬性数量限制或者对大量线程的支撑力不从心的操作系统出现问题。在这些情况下,一个轻量级的选择是非常有用的。如果解决方案在逻辑上可行而又简洁的话,它允许代码分配在不同的线程中执行,并且保持低水平的开销。这个方案的另一个优点是,如果需要调用一个长时间运行的操作或者进行一次系统调用,但又不能阻塞进程中所有代码的执行,那么仍然可以使用内核线程。

Erlang 采用了一个相似的方法,它也提供轻量级进程,但 Erlang 的进程不共享任何东西,与之相反,纤程共享同一个地址空间。然而,线程的存在使得人们可以采用参与式(Actor-style)的编程方式,而不需要担心额外性能开销。

在 Ruby 领域,纤程的概念也不是全新的。Rubinius 拥有 Tasks,其描述和 Ruby 1.9 的纤程类似。(InfoQ 最近有一篇新闻报导采访了Rubinius 项目的领导人 Evan Phoenix 对 Rubinius 中这种线程模型的观点)。MenTaLguY 就此在 ruby-core 上详细描述说

不过,在现代的并发环境中,它们 [纤程] 变得越来越有用。没有它们或者和它们相似的东西(比如 Rubinius 的 Tasks),你就得用一些丑陋的技巧才能实现轻量级并发了——你可以看看在 Scala 的 actors 类库中明确地使用 continuation-passing(指函数,而不是 Continuations),这就是它们还没有问世的时候,我们可以期望的最好的例子。

我同意,使用纤程会让 JRuby 的日子变得不是那么好过。

最后的评论提出了一个重要的观点。如果纤程被 Ruby 采用,那么对于 Ruby 在 JVM 和 CLR 上的实现,如 JRuby、XRuby、Ruby.NET 或者 IronRuby 来说,将出现一个头疼的问题。现在,它们当中还没有一个支持 Continuations,因为在这些虚拟机上操作或者读取调用栈(callstack)是非常困难,或者几乎不可能实现的。是否需要实现 Continuations 是一个很有争议的问题,然而它好像并没有给 JRuby 等带来什么大难题,因为它并没有广泛地应用在 Ruby 之中。在 Ruby 1.8 的标准库中,唯一的应用就是生成器的实现,但是,举例而言,在 JRuby 1.0 中没有使用 Continuations 也一样实现了此功能。

虽然可以避开这些问题来实现这些特性,但是问题在于这些迂回解决方法是否会引起性能下降。例如,如果调用栈必须在堆上仿真,而不是使用虚拟机的栈,这样做可能引起性能的下降,或者会阻止(JIT)编译器的的优化操作。对于非对成协程的迂回解决方法会更容易一些,因为它们使用虚拟机的栈进行方法调用。像 C# 这类的语言以这种方式实现了它们的迭代器(Iterator)特性,这些迭代器允许人们用和上面的示例代码相似的方式编写生成器。

查看英文原文:Ruby 1.9 adds Fibers for lightweight concurrency
译者简介:仝键,网名“咖啡屋的鼠标”,普通程序员。喜欢思考,沉默时沉闷至死,说起来却又无边无际。爱好广泛常恐有贪多不精之后遗症。从小接触电脑却白白荒废十余年光阴,直至大学之后才入编程之门。如今漂泊北京寻找着自己的一片天地。感兴趣的技术领域有 Agile、Java、设计模式、Flex、Ruby 和面向对象数据库等。个人技术博客为http://blog.csdn.net/tj19832/。参与 InfoQ 中文站内容建设,请邮件至china-editorial[at]infoq.com
Ruby编程语言语言 & 开发架构