翻译 Ruby 3 Guilds 并发模型

falm · 2016年10月17日 · 最后由 pynix 回复于 2016年12月21日 · 12368 次阅读
本帖已被管理员设置为精华贴

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

Rei 将本帖设为了精华贴。 10月17日 15:07

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

一修改。在被逼的情况下

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

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

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

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

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

#8 楼 @lilijreey

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

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

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

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

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

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

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

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

@pynix 你的头像很漂亮。

要等到猴年马月去了

#17 楼 @bluealert 不要岔开话题

20 楼 已删除
Mark24 并行并发进程线程协程 GIL 概念简明解释笔记 提及了此话题。 10月13日 14:10
需要 登录 后方可回复, 如果你还没有账号请 注册新账号