BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

Ruby线程模型之未来

| 作者 Werner Schuster 关注 9 他的粉丝 ,译者 Jason lai 关注 0 他的粉丝 发布于 2007年5月28日. 估计阅读时间: 12 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

最近的一次对Ruby创始人Matz(Yukihiro Matsumoto,松本行弘)和YARV创始人笹田耕一(Sasada Koichi)的采访,就Ruby对线程的处理这个话题进行了深入探讨。目前,Ruby的稳定版本使用的是用户空间线程(user space threads,也称为“绿色线程 [green threads]”),也就是说Ruby解释器负责线程调度的每一个细节。这就和内核线程(Kernel Threads)形成了对比,在后者中,线程的创建、调度和同步都是由OS的系统调用完成的,这使得这些操作代价高昂——至少和它们在用户空间线程中的等价物相比确实如此。从另一个角度来说,用户空间线程无法利用多核或者多CPU(因为OS不知道这些线程的存在,因此无法在这些核/CPU上调度它们)。

Ruby 1.9在近期将YARV作为新的Ruby VM整合了进来,这是1.9中带来的改变之一,它将内核线程引入了Ruby。内核线程(也叫“原生线程[native threads]”)的引入赢得了广泛掌声,尤其是来自Java和.NET平台的开发人员更是交口称赞,因为这两个平台下用的就是内核线程。尽管如此,阻碍还是存在着。笹田耕一解释道

大家知道,YARV支持原生线程。这就是说,你可以并发地把每一个Ruby线程运行在每一个原生线程上。

这并不是说,每一个Ruby线程都是并行运行的。YARV里有一把全局VM锁(全局解释器锁),只有唯一在运行的Ruby线程才能拥有。我们大家可能很乐于看见这样的决定,因为我们可以把用C语言写成的大部分扩展运行起来,而不需要进行任何改动。

这就意味着:不管存在着多少个内核或者CPU,只有一个Ruby线程能在任意给定时间里运行。解决方法还是存在的,原生的扩展可以以更加灵活的方式处理全局解释器锁(Global Interpreter Lock,GIL),例如,在开始长时间操作之前将锁释放。笹田耕一解释了释放GIL的可用API

你必须在进行阻塞操作之前释放巨型VM锁。如果你需要在扩展库中这么做,请使用rb_thread_blocking_region()这个API。

API:
rb_thread_blocking_region (
blocking_func, /* function that that will block */
data, /* this will be passed above function */
unblock_func /* if another thread cause exception with Thread#raise,
this function is called to unblock or NULL */
)

问题在于:这样做有效地排除了对内核线程最大的赞成观点——对多核或者多CPU的利用,而又保留了内核线程的问题。

内核线程的引入也是Continuation可能在今后的Ruby版本中移除的原因。Continuation是协作调度(cooperative scheduling)的一种方式,即吧一个线程中执行的操作显式地转给另一个来控制。这个特性也以“协同程序(Coroutine)”之名为人所知,并且存在了很长的一段时间。最近,因为基于Smalltalk的Web框架Seaside使用了Continuation非常显著地简化了Web应用,它也逐渐开始在公众眼前亮相。

这个结合GIL使用内核线程的方式和Python的线程系统是很相似的,后者同样使用GIL,而且用了很长一段时间。Python的GIL引发了无数论战,探讨怎样将其移除,尽管争论热火朝天,GIL仍然悍然不动。

然而,我们考察一下Python语言的创立者Guido van Rossum对于线程的看法,不难发现Ruby线程调度可选的一条未来之路。在最近一篇关于GIL的帖子里,Guido van Rossum解释说

然而,没错,GIL并不像你最初想象的那么坏:你要做的就是赶快从Windows和Java支持者的洗脑中恢复过来,他们似乎认为线程仅仅是同步活动的唯一实现方式。

仅仅因为Java曾经以在不能支持多地址空间的机顶盒OS上运行作为目标,或者只是由于在Windows中创建进程曾经慢得和狗一样,并不意味着与线程相比,多进程(加上对IPC的合理使用)对于多CPU机器来说就不是一种好多得多的方法。

你所要做的,就是对加锁、死锁、锁的粒度、活锁、非确定性和竞争条件(race conditions)的邪恶组合说“不”。

关于共享地址空间、有抢先调度权的线程所能带来的益处的争论由来已久。Unix作为单线程或者用户空间线程系统的时间最长,它的并行操作是以多个线程通过不同的进程间通信(InterProcess Communication,IPC)的形式(如管道、先进先出[FIFOs]或者显式共享的内存区)来实现的。这是通过fork系统调用的方式支持的,这种方式可以以低廉的代价复制正在运行的进程。

近来,诸如Erlang之类的语言因为同样使用了一种无共享(share nothing)的方式(也称为“轻量级进程”)+简单的IPC方法,开始受到青睐。“轻量级进程(lightweight processes)”并不是OS进程,实际上存在于相同的地址空间之内。它们之所以被称为“进程”,是因为它们无法窥探彼此的内存空间。“轻量级”则是由于它们是由用户空间的调度程序处理的。在很长一段时间内,这意味着Erlang拥有和其它在用户空间进行线程调度的系统一样的问题:不支持多核或者多CPU,并且阻塞性系统调用将阻塞所有线程。不过最近,有人采用了m:n的方式解决了这些问题:目前Erlang运行时使用多个内核线程,每个线程都运行着一个用户空间调度器。这就是说,现在Erlang可以利用多核或者CPU,而且不需要改变自身的运行模式。

Ruby社区很有幸,Ruby团队已经知晓此事,并且考虑将它作为Ruby线程调度的未来方向

[...]如果我们在一个进程内有多个VM实例,这些VM就可以同步运行。我会在近期着手此事(作为我的研究课题)。

[...]如果原生线程存在许多许多问题,我将实现绿色线程。大家知道,相比原生线程它有一些优势(如轻量级线程创建等)。这会是一次很有趣的Hack过程(跟大家说一声,我的毕业论文就是在我们特定的SMT CPU上实现用户级线程库)。

这就表明,Ruby的用户空间(绿色)线程版本并不会从议事桌上撤离,特别是因为在不同OS上线程系统的实现问题,例如这个问题

使用原生线程编写代码有它自身的问题。例如,在MacOSX上,如果有其它线程运行的话(,exec()不能正常工作(会引发异常)(这是移植性问题之一)。如果我们在使用原生线程时发现严重问题,我会把绿色线程的版本放到代码主干上(YARV)。

为什么会需要笹田耕一的多VM(Mutilple VM)方案呢?运行多个Ruby解释器,并使它们以IPC方式进行通信(例如,通过Socket)在今天看来也是可能的。然而,这样会带来一系列问题:

  • Ruby进程需要运行一个新的Ruby解释器,这就意味着进程必须了解自己是如何启动的(应该使用哪个Ruby可执行程序)。这很快会变得很难通过一种可移植的方式完成。举例而言:如果使用了JRuby,那么可执行程序就该是“JRuby”。更糟糕的情况:运行它的JVM或者应用服务器可能不会允许在程序之外运行;
  • 新的Ruby解释器必须使用正确的环境变量、LOADPATHs、包含文件路径和要运行的主.rb文件设置起来;
  • 通信可以通过DRb来产生,但必须由网络来完成,而这是唯一一种具备可移植性的IPC形式;
  • 网络通信就意味着要用到协商端口(即哪一个端口应成为两个互相监听的程序的“服务端”);
  • 网络通信还意味着与防火墙之间可能存在的问题,如受到程序打开连接或者端口的困扰。

当然,这些问题导致了这种方法比用Thread来启动一个新的执行线程要复杂得多:

x = Thread.new {
p "hello"
}

或者也比这个Erlang范例要复杂:

pid_x =  spawn(Node, Mod, Func, Args)

这段Erlang代码产生了一个新的轻量级进程,而且这确确实实就是所有它需要的代码。所有的配置代码都已经处理好了,问题中没有一个解释了上面的原因。

这个pid是新产生进程的句柄,并且支持如简单通讯这样的操作:

pid_x ! a_message

这段代码会向pid_x变量存放的pid对应的线程发送一条简单消息。消息可以包含不同的类型,例如原子(Atoms)——Erlang下的Ruby符号(Symbol)等价物。

像这样简单的IPC在Ruby中理所当然可以实现。Erlectricity是一个新的支持Erlang和Ruby间通信的库,但它同样可以用在Ruby VM之间。Erlang IPC尤其有意思,因为它使用了一个模式匹配的方式来辅助消息传递,并使自身变得非常简洁。

毫无疑问,Ruby MVM是对Ruby线程的未来最有希望的设想。它避免了GIL和手动管理Ruby进程的问题,并且使用了“无共享”的理念,这个理念使得Erlang还有其它系统在并行计算方面非常引人注目。

JRuby是唯一一个使用内核线程的Ruby实现,主要原因在于它运行在支持内核线程的JVM之上。创建内核线程的开销在一定程度上因为线程池(事先创建出若干空闲线程,并在需要时取用)的使用而抵消掉了。IronRuby对线程进行支持的细节目前尚不清楚,但由于CLR和JVM非常相近,它很可能也会使用内核线程。

为Ruby MVM的想法创建原型并进行实验的可能性之一,就是在同一个JVM内部启动多个JRuby实例,并让它们之间互相通信。这样就能很有效地带来同样的低廉的IPC(只要数据是只读的,它们就可以很容易通过传递指针的方式传递)。

Ola Bini最近撰文阐述了他关于jrubysrv的想法,这个想法允许运行在一个JVM内部运行多个JRuby实例,以节省内存。

看起来未来在Ruby中进行线程支持的细节仍然有待决定,并且可能在不同的实现中各有差别。

查看英文原文:The Futures of Ruby Threading

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

值得推荐的新闻,深入有见地 by Lai Jason

对 Ruby 标准实现采用用户空间线程的争论,一直是人们在质疑 Ruby 是否能企业化的主要论据之一。

这篇新闻写得很深入,从多个角度分析了 Ruby 目前的线程模型,并借鉴了 Python、Erlang 等多个动态语言的线程系统,结合 Ruby 核心团队的观点,对 Ruby 运行时未来可能支持的线程模型进行了展望。

如果 Ruby 1.9 以后真正能支持原生线程的话,那么说明 Ruby 向企业化又迈进了一大步。

我个人还是很看好使用原生线程+线程池的想法。

不过文章似乎没有讨论到其他的 Ruby 实现,比如 Ribinius 和 XRuby。前者似乎能够支持多种线程模型,包括用户空间线程和轻量级进程模型。后者,由于采用的是将 Ruby 代码编译成 Java 字节码的方式,我想应该只能支持原生线程了吧?是不是可以在今后默认采用线程池的方式,并由 runtime 来进行管理,以减小线程创建的开销呢?

XRuby的线程模型 by Yu Zhang

XRuby目前采用的是Native thread模型,主要是因为JVM采用的就是这个模型。由于Native thread模型是在OS上包了一层,因此Java,XRuby的语义具有不确定性,这将引起一系列的问题,比如同步,加锁,锁粒度,死锁等等。如果想更详细的了解这些问题,可以参考比较新的论文《The Problem with Threads》。
多核的出现,使得软件人员必须去解决Concurrent问题。目前,Erlang语言提供了一个很好的方向,它抛弃了大家熟悉的Thread模型。

目前,打算借鉴Scala,在XRuby里采用与Erlang类似的并发范式。

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

2 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT