文本为翻译Olivier Lacan在其博客上发布的文章,并对内容上做了适当的调整。
英文原文地址: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就提供了线程互斥锁,但它或多或少违反了并行的初衷,『锁』容易使程序变慢,而且不当使用的话,很可能使得并发程序比相同的同步程序还要慢。
Guilds工作原理
在Koichi的允许下,我为了让它更好理解,就对他的提案做了适当的缩减。
Guild是基于现有的 线程(thread)和纤程(fiber)实现的。它至少由一个线程组成,并且该线程至少由一个纤程组成。不同Guild中的线程可以并行运行,而相同Guild中的线程不能。一个Guild不能对其他Guild中的对象进行读写。
同一个Guild中的不同线程不能并发运行,这是因为Guild存在一个叫 GGL(Giant Guild Lock)的锁,它是用来确保线程是先后运行的,而不同Gulid的线程是可以并发运行的。
你也可以认为 Ruby 2.x 的程序,就相等于是存在一个Guild的程序。
Guilds间通信
不同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
Guild对比线程的优势
在线程中,我们很难判别出,哪些可变对象已经被共享了。而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在并发友好性上,做出了积极的影响。