1 分钟

Ruby Ractor和解释语言中的并行性

Ractor

Ruby 3.0 的主要特性之一是引入了“ractor”。 Ractor 是 Ruby 的一个新的并发抽象。 与 Ruby 线程不同,ractor 可以并行运行。 由于 Ruby 全局虚拟机锁(GVL),即使每个 Ruby 线程都映射到本地线程,也只能有一个活动线程在运行。 但是引入 ractor 后,可以有多个 GVL,每个 ractor 有一个。 这就是 ractor 可以并行执行的原因。

ractor 的引入使 Ruby 能够实现真正的并行性,同时使用一个解释器。 在我们深入研究 ractor 实现之前,我想首先谈谈为什么 Ruby 有 GVL。

Global VM Lock

Python 中的 GVL 或 GIL(全局解释器锁)。它是一种解释器范围的锁,可确保在任何给定时间只有一个线程正在执行。它的存在是为了方便解释器的开发。要理解为什么只有解释型语言有这个而其他语言没有,我们需要知道解释器是做什么的。 Ruby 允许动态添加类/模块的实例/类变量,它将这些信息存储在一个表中。现在,如果两个线程正在修改这个表,就会发生数据竞争。解释器所做的另一件事是垃圾收集。关于垃圾收集的信息存储在“对象空间”中,垃圾收集器将遍历这个空间,删除每个未被引用的对象。现在,如果一个线程触发垃圾收集器但另一个线程仍在创建对象,数据竞争也会发生。

所以如果你有GVM,就不用担心上面的问题了。像 Java 这样的语言不允许在运行时修改类变量。而且关于垃圾收集的信息并不存储在一个集中的空间中,它单独存储在每个对象本身上。

在Ractor之前的并发实现

为了实现并行性,Ruby on Rails 产生了几个新的 Ruby 进程。 因为每个 Ruby 进程都有自己的 GVL。 但是我们现在有多个解释器,这使得我们的程序效率低下。 同样,Python 的 Flask 也使用这种方法。 至于 Node.js,它也催生了新的 Js 引擎。 但是 Js 没有 GVL,因为它没有线程。

Ractor是如何解决这些问题的

Ractor 通过限制对主要 Ractor 的访问来“解决”实例/类变量的数据竞争。一个线程不会造成数据竞争,嗯?

对于垃圾收集,每次垃圾收集器运行时,其他 Ractor 的所有执行都会暂停。

第三,对象不在 Ractor 之间共享。 Ractor 只能访问 frezee 或“特殊可共享对象”之一的对象(Ractor 本身)。要在 Ractor 之间共享可变对象,您需要“移动”它(从而使其在原始 Ractor 中不可用)或深度复制它。基本上将通信方法限制为进程间通信方法。

所以 Ractor 通过不解决它们来“解决”这些问题!是的,您无需多个进程即可获得真正的并行性。但是您只能像拥有多个进程一样使用它!换句话说,Ruby Ractor 不是并行线程,它更像是“Process with Benefits”。您可以像使用过程一样使用它,而无需承担创建新解释器的成本。 :P

它好用吗?

官方文档中有很多关于 Ractor 的例子。它几乎让我相信 Ractor 是如此强大,以至于我可以将我的生产代码转换为使用 Ractor,并在一天内实现 10 倍的性能。好吧,由于我上面提到的限制,将现有代码库转换为 Ractor 非常困难。考虑一个用于对某些对象进行排名的类。您可能有一个哈希值将值与优先级相关联。现在假设它是一个泛型类,实际支持的排名对象值需要在运行时确定。这意味着需要在运行时修改哈希。执行此操作的常用方法是在需要时使用延迟初始化来更新哈希。但是我们的 Ractor 期望类变量被冻结,所以我们需要事先冻结散列。但是,在开始对对象进行排序之前,我们不知道散列中应该包含什么! autoload||= 和猴子补丁的使用使得将现有程序转换为使用 Ractor 几乎是不可能的。特别是如果您使用第三方 Gems。在任何人都可以使用 Ractors 之前,必须交替程序架构。

如果每个人在编写 Ruby 代码时都考虑到 Ractor,那么从 Ractor 中获益会容易得多。然而,Ractor 仅在 Ruby 3.0 中上线,并且大多数 Ruby 代码是在 2.x 时代编写的。重写庞大的代码库需要很长时间。 Ractor 目前的实验状态意味着这项工作不会很快发生。

结论

我并不是说 Ractor 是失败的,它是一个很好的功能。 通过将真正需要锁的地方与解释器的其余部分分开,这是迈向fine-grained lock的一步。 在设计为具有 GVL 的语言上实现 Ractor 本身就是一个挑战。

最新文章

分类

关于

A young developer who loves Linux.