翻译 Ruby 3 Guilds 并发模型

falm · 发布于 2016年10月17日 · 最后由 pynix 回复于 2016年12月21日 · 3726 次阅读
E0fcf5
本帖已被设为精华帖!

文本为翻译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就提供了线程互斥锁,但它或多或少违反了并行的初衷,『锁』容易使程序变慢,而且不当使用的话,很可能使得并发程序比相同的同步程序还要慢。

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在并发友好性上,做出了积极的影响。

共收到 18 条回复
1 Rei 将本帖设为了精华贴 10月17日 15:07
27

2020 年,有点慢啊,Rails 6 估计都赶不上

24996

一修改.在被逼的情况下

23529

#3楼 @lilijreey 前言不搭后语,nodejs还要学?现在用em就能实现一模一样的东西。immutable的值为什么不能共享?

20

还是觉得 Actor 这种模型要更优秀。

De6df3

#3楼 @lilijreey Node.js 也是单线程...

24996

#5楼 @mizuhashi Actor是王道,你还太嫩

24996

#7楼 @huacnlee 单线程就够好了. 一个语言或者环境如果要提供并发机制,最好是隔离并发. 拷贝复制,才能提升整体的性能. 锁是邪恶的.

23529

#8楼 @lilijreey

正面回答我的问题,immutable的值为什么不能共享?

别以为知道个名词就能来掉书袋,joe的书我又不是没看过

23529

#9楼 @lilijreey Actor的信箱本来就是锁

9442

#11楼 @mizuhashi 实现actor模型离不开锁,但是用户使用actor就不用关心锁了。😄,erlang入门者。

8311

#9楼 @lilijreey 来来来,麻烦评价下 Clojure STM 机制怎么样?

24996

#13楼 @geekerzp 不熟,无法评论

24996

#10楼 @mizuhashi 不是不能,是不要. 我说的都是对开发友好的方式. 如果你非要钻牛角尖. 给你一个场景你就知道了. 共享不变量对GC实现的影响. 并发在业务层面是有价值的.但是在执行层面价值不大. 因为CPU基本上都是乱序执行指令的. 并发更多的是解决IO阻塞和加快响应速度. 在纯计算上,单线程+消息通信是最好的实现模式.

9800

用起来很繁琐的样子。。还是没有语言级别的并发原语。。。。

96

@pynix 你的头像很漂亮。

9592

要等到猴年马月去了

9800

#17楼 @bluealert 不要岔开话题

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册