我知道 ruby 使用绿色线程的“合作”线程。如何在我的应用程序中创建真正的“操作系统级”线程以利用多个 cpu 内核进行处理?
9 回答
更新了 Jörg 2011 年 9 月的评论
您似乎在这里混淆了两个非常不同的东西:Ruby 编程语言和 Ruby 编程语言的一种特定实现的特定线程模型。目前大约有 11 种不同的 Ruby 编程语言实现,具有非常不同和独特的线程模型。
(不幸的是,这 11 个实现中只有两个实际上已准备好用于生产,但到今年年底,这个数字可能会上升到四个或五个。)(更新:现在是 5 个:MRI、JRuby、YARV(解释器Ruby 1.9)、Rubinius 和 IronRuby)。
第一个实现实际上没有名字,这使得引用它很尴尬,而且真的很烦人和令人困惑。它最常被称为“Ruby”,这比没有名字更令人讨厌和困惑,因为它会导致 Ruby 编程语言的特性和特定的 Ruby 实现之间无休止的混淆。
它有时也称为“MRI”(代表“Matz 的 Ruby 实现”)、CRuby 或 MatzRuby。
MRI 在其解释器中将 Ruby 线程实现为绿色线程。不幸的是,它不允许并行调度这些线程,它们一次只能运行一个线程。
但是,任何数量的 C 线程(POSIX 线程等)都可以与 Ruby 线程并行运行,因此外部 C 库或创建自己的线程的 MRI C 扩展仍然可以并行运行。
第二个实现是YARV(“Yet Another Ruby VM”的缩写)。YARV 将 Ruby 线程实现为 POSIX 或 Windows NT 线程,但是,它使用全局解释器锁 (GIL) 来确保在任何时候实际上只能调度一个 Ruby 线程。
与 MRI 一样,C 线程实际上可以与 Ruby 线程并行运行。
将来,GIL可能会分解成更细粒度的锁,从而允许越来越多的代码实际并行运行,但这还很遥远,甚至还没有计划。
JRuby 将 Ruby 线程实现为 Native Threads,其中 JVM 的“Native Threads”显然意味着“JVM Threads”。JRuby 没有对它们施加额外的锁定。因此,这些线程是否可以真正并行运行取决于 JVM:一些 JVM 将 JVM 线程实现为 OS 线程,而另一些则实现为绿色线程。(从 JDK 1.3 开始,Sun/Oracle 的主流 JVM 只使用 OS 线程)
XRuby还将Ruby 线程实现为 JVM 线程。更新:XRuby 已经死了。
IronRuby 将 Ruby 线程实现为 Native Threads,其中 CLR 中的“Native Threads”显然意味着“CLR Threads”。IronRuby 没有对它们施加额外的锁定,因此,它们应该并行运行,只要您的 CLR 支持。
Ruby.NET还将Ruby 线程实现为 CLR 线程。更新: Ruby.NET 已经死了。
Rubinius 在其虚拟机中将 Ruby 线程实现为绿色线程。更准确地说:Rubinius VM 导出了一个非常轻量级、非常灵活的并发/并行/非本地控制流构造,称为“任务”,以及所有其他并发构造(本讨论中的线程,还有Continuations、Actor和其他东西) 在纯 Ruby 中使用 Tasks 实现。
但是,Rubinius 不能(当前)并行调度线程,但补充说这并不是什么大问题:Rubinius 已经可以在一个 Rubinius 进程中并行运行多个 POSIX 线程中的多个 VM 实例。由于线程实际上是在 Ruby 中实现的,因此它们可以像任何其他 Ruby 对象一样被序列化并发送到不同 POSIX 线程中的不同 VM。(这与 BEAM Erlang VM 用于 SMP 并发的模型相同。它已经为 Rubinius Actors 实现了。)
更新:此答案中有关 Rubinius 的信息是关于 Shotgun VM 的,它不再存在。“新”C++ VM 不使用跨多个 VM 调度的绿色线程(即 Erlang/BEAM 样式),它使用具有多个本机操作系统线程模型的更传统的单个 VM,就像 CLR、Mono 所采用的模型一样,以及几乎每个 JVM。
MacRuby最初是作为 YARV 在 Objective-C 运行时和 CoreFoundation 和 Cocoa 框架之上的一个端口。它现在与 YARV 明显不同,但 AFAIK 目前仍与 YARV 共享相同的线程模型。 更新: MacRuby 依赖于苹果垃圾收集器,该垃圾收集器已被声明为已弃用,并将在以后的 MacOSX 版本中删除,MacRuby 是不死的。
Cardinal是Parrot 虚拟机的 Ruby 实现。它还没有实现线程,但是,当它实现时,它可能会将它们实现为Parrot Threads。更新:红衣主教似乎非常不活跃/死了。
MagLev是GemStone/S Smalltalk VM的 Ruby 实现。我不知道 GemStone/S 使用什么线程模型,MagLev 使用什么线程模型,或者即使线程已经实现(可能还没有)。
HotRuby本身并不是一个完整的 Ruby 实现。它是 JavaScript 中 YARV 字节码 VM 的实现。HotRuby 不支持线程(还没有?),当它支持时,它们将无法并行运行,因为 JavaScript 不支持真正的并行性。不过,HotRuby 有一个 ActionScript 版本,而且 ActionScript 可能实际上支持并行性。更新:HotRuby 已经死了。
不幸的是,这 11 个 Ruby 实现中只有两个实际上是生产就绪的:MRI 和 JRuby。
所以,如果你想要真正的并行线程,JRuby 目前是你唯一的选择——这并不是一个坏的选择:JRuby 实际上比 MRI 更快,并且可以说更稳定。
否则,“经典” Ruby 解决方案是使用进程而不是线程来实现并行性。Ruby 核心库包含带有
方法的Process
模块,这使得分叉另一个 Ruby 进程变得非常容易。此外,Ruby 标准库包含
分布式 Ruby (dRuby / dRb)库,它允许 Ruby 代码在多个进程中轻松分布,不仅在同一台机器上,而且在网络上。Process.fork
Ruby 1.8 只有绿色线程,没有办法创建真正的“OS 级”线程。但是,ruby 1.9 将有一个新特性,称为纤维,它允许您创建实际的操作系统级线程。不幸的是,Ruby 1.9 仍处于测试阶段,计划在几个月内稳定下来。
另一种选择是使用 JRuby。JRuby 将线程实现为操作系统级别的线程,其中没有“绿色线程”。JRuby的最新版本是1.1.4,相当于Ruby 1.8
这取决于实现:
- MRI没有,YARV更接近。
- JRuby 和 MacRuby 都有。
Ruby 有闭包Blocks
,lambdas
和Procs
. 为了充分利用 JRuby 中的闭包和多核,Java 的执行器就派上用场了;对于 MacRuby,我喜欢GCD 的 queues。
请注意,能够创建真正的“操作系统级”线程并不意味着您可以使用多个 cpu 内核进行并行处理。看看下面的例子。
这是使用 Ruby 2.1.0使用 3 个线程的简单 Ruby 程序的输出:
(jalcazar@mac ~)$ ps -M 69877
USER PID TT %CPU STAT PRI STIME UTIME COMMAND
jalcazar 69877 s002 0.0 S 31T 0:00.01 0:00.04 /Users/jalcazar/.rvm/rubies/ruby-2.1.0/bin/ruby threads.rb
69877 0.0 S 31T 0:00.01 0:00.00
69877 33.4 S 31T 0:00.01 0:08.73
69877 43.1 S 31T 0:00.01 0:08.73
69877 22.8 R 31T 0:00.01 0:08.65
正如您在此处看到的,有四个操作系统线程,但是只有一个有状态的线程R
正在运行。这是由于 Ruby 线程实现方式的限制。
相同的程序,现在使用 JRuby。您可以看到三个带有 state 的线程R
,这意味着它们是并行运行的。
(jalcazar@mac ~)$ ps -M 72286
USER PID TT %CPU STAT PRI STIME UTIME COMMAND
jalcazar 72286 s002 0.0 S 31T 0:00.01 0:00.01 /Library/Java/JavaVirtualMachines/jdk1.7.0_25.jdk/Contents/Home/bin/java -Djdk.home= -Djruby.home=/Users/jalcazar/.rvm/rubies/jruby-1.7.10 -Djruby.script=jruby -Djruby.shell=/bin/sh -Djffi.boot.library.path=/Users/jalcazar/.rvm/rubies/jruby-1.7.10/lib/jni:/Users/jalcazar/.rvm/rubies/jruby-1.7.10/lib/jni/Darwin -Xss2048k -Dsun.java.command=org.jruby.Main -cp -Xbootclasspath/a:/Users/jalcazar/.rvm/rubies/jruby-1.7.10/lib/jruby.jar -Xmx1924M -XX:PermSize=992m -Dfile.encoding=UTF-8 org/jruby/Main threads.rb
72286 0.0 S 31T 0:00.00 0:00.00
72286 0.0 S 33T 0:00.00 0:00.00
72286 0.0 S 31T 0:00.09 0:02.34
72286 7.9 S 31T 0:00.15 0:04.63
72286 0.0 S 31T 0:00.00 0:00.00
72286 0.0 S 31T 0:00.00 0:00.00
72286 0.0 S 31T 0:00.00 0:00.00
72286 0.0 S 31T 0:00.04 0:01.68
72286 0.0 S 31T 0:00.03 0:01.54
72286 0.0 S 31T 0:00.00 0:00.00
72286 0.0 S 31T 0:00.01 0:00.01
72286 0.0 S 31T 0:00.00 0:00.01
72286 0.0 S 31T 0:00.00 0:00.03
72286 74.2 R 31T 0:09.21 0:37.73
72286 72.4 R 31T 0:09.24 0:37.71
72286 74.7 R 31T 0:09.24 0:37.80
相同的程序,现在使用 MacRuby。还有三个线程并行运行。这是因为MacRuby 线程是 POSIX 线程(真正的“操作系统级”线程)并且没有 GVL
(jalcazar@mac ~)$ ps -M 38293
USER PID TT %CPU STAT PRI STIME UTIME COMMAND
jalcazar 38293 s002 0.0 R 0T 0:00.02 0:00.10 /Users/jalcazar/.rvm/rubies/macruby-0.12/usr/bin/macruby threads.rb
38293 0.0 S 33T 0:00.00 0:00.00
38293 100.0 R 31T 0:00.04 0:21.92
38293 100.0 R 31T 0:00.04 0:21.95
38293 100.0 R 31T 0:00.04 0:21.99
再一次,同样的程序,但现在有了旧的 MRI。由于此实现使用绿色线程,因此仅显示一个线程
(jalcazar@mac ~)$ ps -M 70032
USER PID TT %CPU STAT PRI STIME UTIME COMMAND
jalcazar 70032 s002 100.0 R 31T 0:00.08 0:26.62 /Users/jalcazar/.rvm/rubies/ruby-1.8.7-p374/bin/ruby threads.rb
如果您对 Ruby 多线程感兴趣,您可能会发现我的报告使用 fork 处理程序调试并行程序很有趣。
对于 Ruby 内部结构的更一般的概述,Ruby Under a Microscope是一本不错的读物。
此外, Omniref 中的Ruby 线程和 C 中的全局解释器锁在源代码中解释了为什么 Ruby 线程不能并行运行。
使用drb怎么样?它不是真正的多线程,而是多个进程之间的通信,但您现在可以在 1.8 中使用它,而且摩擦力相当低。
我会让“系统监视器”回答这个问题。在这两种情况下,我都在 i7(4 个超线程核心)机器上运行 8 个 Ruby 线程执行相同的代码(下面,它计算素数)......第一次运行是:
jruby 1.5.6 (ruby 1.8.7 patchlevel 249) (2014-02-03 6586) (OpenJDK 64-Bit Server VM 1.7.0_75) [amd64-java]
第二个是:
红宝石 2.1.2p95 (2014-05-08) [x86_64-linux-gnu]
有趣的是,JRuby 线程的 CPU 更高,但解释型 Ruby 的完成时间略短。从图中很难看出,但第二次(解释为 Ruby)运行使用了大约 1/2 的 CPU(没有超线程?)
def eratosthenes(n)
nums = [nil, nil, *2..n]
(2..Math.sqrt(n)).each do |i|
(i**2..n).step(i){|m| nums[m] = nil} if nums[i]
end
nums.compact
end
MAX_PRIME=10000000
THREADS=8
threads = []
1.upto(THREADS) do |num|
puts "Starting thread #{num}"
threads[num]=Thread.new { eratosthenes MAX_PRIME }
end
1.upto(THREADS) do |num|
threads[num].join
end
如果您使用 MRI,那么您可以在 C 中编写线程代码作为扩展或使用 ruby-inline gem。
如果您真的需要 Ruby 中的并行性来实现生产级系统(不能使用 beta 版),那么流程可能是更好的选择。
但是,绝对值得首先尝试 JRuby 下的线程。
此外,如果您对 Ruby 下线程的未来感兴趣,您可能会发现这篇文章很有用。
这是关于 Rinda 的一些信息,它是 Linda 的 Ruby 实现(并行处理和分布式计算范式)http://charmalloc.blogspot.com/2009/12/linda-tuples-rinda-drb-parallel.html
因为无法编辑那个答案,所以在这里添加一个新的回复。
更新(2017-05-08)
这篇文章很老了,信息不符合当前(2017)的步伐,以下是一些补充:
Opal是一个 Ruby 到 JavaScript 源代码到源代码的编译器。它还有一个 Ruby corelib 的实现,它目前非常活跃,并且存在大量(前端)框架在它上面工作。并准备好生产。因为基于javascript,它不支持并行线程。
truffleruby是 Ruby 编程语言的高性能实现。TruffleRuby 由 Oracle Labs 在 GraalVM 上构建,是 JRuby 的一个分支,将它与来自 Rubinius 项目的代码相结合,还包含来自 Ruby、MRI 的标准实现的代码,仍然是实时开发,而不是生产就绪。这个版本的 ruby 似乎是为性能而生的,我不知道是否支持并行线程,但我认为应该。