文本为翻译 Olivier Lacan 在其博客上发布的关于 Ruby 3 Guilds 介绍的文章。我对内容上做了适当的调整,这里给大家分享一下,翻译水平有限,见谅了。
英文原文地址:http://olivierlacan.com/posts/concurrency-in-ruby-3-with-guilds/
在 Ruby Kaigi 2016 大会上,Ruby VM 和 GC 的作者 Koichi Sasada 为 Ruby 3 提出了一个新的并发模型。
我们都知道 Ruby 提供了线程去实现并发,但是 MRI 并不能让 Ruby 的代码并行的运行。Koichi 致力于解决 Ruby 并行中,遇到的各种挑战,包括可变对象,竞态条件和线程间同步,最终他提出了一个新的并行并发机制,称为 Guilds。
如果你对并发和并行不太了解,可以先阅读一下这篇文章:并发与并行的区别。
为 Ruby3 提出 Guilds 的基本前提是,保证与 Ruby2 兼容的情况下实现并行,考虑到 GIL 限制了并行,所以要尝试通过使用快速对象共享和特殊结构对象,来实现共享可变对象。
实现并发在目前的 Ruby 版本中是一件很困难的事情,这因为程序员需要手动的管理线程,确保不会出现竞态条件。通用的做法是在线程中引入『锁』,像 Ruby 就提供了线程互斥锁,但它或多或少违反了并行的初衷,『锁』容易使程序变慢,而且不当使用的话,很可能使得并发程序比相同的同步程序还要慢。
在Koichi的允许下,我为了让它更好理解,就对他的提案做了适当的缩减。
Guild 是基于现有的 线程 (thread) 和纤程 (fiber) 实现的。它至少由一个线程组成,并且该线程至少由一个纤程组成。不同 Guild 中的线程可以并行运行,而相同 Guild 中的线程不能。一个 Guild 不能对其他 Guild 中的对象进行读写。
同一个 Guild 中的不同线程不能并行运行,这是因为 Guild 存在一个叫 GGL(Giant Guild Lock) 的锁,它是用来确保线程是先后运行的,而不同 Gulid 的线程是可以并行运行的。
你也可以认为 Ruby 2.x 的程序,就相等于是存在一个 Guild 的程序。
不同 Guild 间不可以相互读写可变对象,这保证了 Guild 并发运行时,不会因为并发读写而引发问题。
不过,Guild 可以通过Guild::Channel
提供的接口,将对象复制或移动到其他的 Guild 中。
Guild::Channel
-transfer(object)
可以将一个对象深度拷贝到目标 Guild 中。
同样也可以使用,Guild::Channel
中的transfer_membership(object)
将一个对象完全移动到其他的 Guild 中。
一旦对象被移动到新的 Guild 后,如果原来持有该对象的 Guild 再对它进行访问,将会抛出异常。
这里我们就知道了,Guild 是不能在没有复制或移动的情况下共享可变对象的,还有一点非常重要,那就是不可变对象(immutable objects)在『深度冻结』(意思是被该对象引用的对象也是冻结不可变的)的情况下是可以直接在多个 Guild 间共享的。
下面这个例子,展示可变和不可变对象的区别:
# 像整数,这样的数值类型,默认就是不可变的,而哈希不是。
mutable = [1, { "key" => "value" }, 3].freeze
# 而如果 数组实例及其引用的,字符串和哈希的实例都进行冻结,
# 就会得到一个"深度冻结" 的不可变对象。
immutable = [
"bar".freeze,
{ "key" => "value".freeze }.freeze
].freeze
在 Koichi Sasada 的研究中,他给出了几个关于如何使用 Guilds 的例子,我在这里将其中最小的,关于使用 Guild 并行计算『斐波那契』的例子进行了简化进行展示。
def fibonacci(n)
return n if n <= 1
fibonacci( n - 1 ) + fibonacci( n - 2 )
end
guild_fibonacci = Guild.new(script: %q{
channel = Guild.default_channel
while(n, return_channel = channel.receive)
return_channel.transfer( fibonacci(n) )
end
})
channel = Guild::Channel.new
guild_fibonacci.transfer([3, channel])
puts channel.receive
在线程中,我们很难判别出,哪些可变对象已经被共享了。而 Guilds 禁止可变对象的共享,转而提供一个简单的方式去共享不可变对象,而且 Koichi 计划,通过使用『特殊数据结构』来共享可变对象,『特殊数据结构』会自动隔离有风险的可变代码。
当然,使用 Guilds 相较于线程来说,有些繁琐,这也是需要有取舍的。
我的理解是,目前 Guilds 的"C"实现仅有 400 行代码,虽然该实现现阶段还不能用,但 Koichi 展示了,运行多个 Guilds 相比于运行单个 Guilds 在斐波那契例子上的性能优势。
在双核的 Linux 虚拟机上运行 Window 7,Koichi 观察到以下结果:
这也许不能代表,现实场景中大部分 Ruby 应用会得到性能上的提升,但我还是等不及想知道,Guilds 在RubyBench上的测试结果。
Guilds 是一种面向 Ruby 的,简单易用并且安全的并发方式,非常希望 Koichi Sasada 和 Ruby 核心团队能在之后,分享更多关于 Guilds 的信息。
我对 Guilds 中移动和复制对象的方法的命名,有一些小看法,因为Guild::Channel. transfer(object)
看上去更像是交换的意思,而结果仅仅是对象的深度拷贝,我相信transfer_copy(object)
或是更简单的copy(object)
更加适合。还有transfer_membership(object)
移动对象的方法,可以简化命名为channel.transfer(object)
。当然了这些方法的命名也不会是一成不变的。
我非常期望,Ruby 核心团队能够将这个新功能,作为实验性质的可选功能发布出去,这样 Ruby 社区就能参与进去帮助改进和测试。还可以让我们在 2020 年的 Ruby3 版本之前,就使用上这个新特性。Guilds 将会对 Ruby 在并发友好性上,做出了积极的影响。