并发指南

本指南旨在帮助理解 cruby 源代码中的并发问题,无论是通过编写 C 代码为 Ruby 做贡献,还是为 JIT 贡献。本指南不涉及原生扩展,仅关注核心语言。它将涵盖:

什么需要同步?

在 Ractor 出现之前,一次只有一个 Ruby 线程可以运行。但这并不意味着你可以忽略并发问题。定时器线程是一个原生线程,它与其他 Ruby 线程交互并修改一些 VM 内部状态,因此如果这些修改可能同时被定时器线程和 Ruby 线程并行执行,就需要进行同步。

当加入 Ractor 时,情况变得更加复杂。然而,Ractor 允许你忽略不可共享对象的同步问题,因为它们不会在 Ractor 之间共享。一次只有一个 Ruby 线程可以访问该对象。对于可共享对象,它们被深度冻结,因此对象本身不会发生变异。但是,例如在 Ractor 之间读取/写入常量确实需要同步。在这种情况下,Ruby 线程需要看到一致的 VM 视图。如果更新的发布需要 2 步甚至两个单独的指令,如本例所示,则需要同步。

大多数同步是为了保护 VM 内部状态。这些内部状态包括每个 Ractor 上的线程调度器结构、全局 Ractor 调度器、Ruby 线程和 Ractor 之间的协调、全局表(用于 fstrings、编码、符号和全局变量)等。任何可以被一个 Ractor 修改,同时又可能被另一个 Ractor 读取或修改的内容,都需要适当的同步。

VM 锁

只有一个 VM 锁,它用于只能由一个 Ractor 一次进入的临界区。没有 Ractor 时,VM 锁是无用的。它不会阻止所有 Ractor 运行,因为 Ractor 可以不尝试获取此锁而运行。如果你正在更新 Ractor 之间共享的全局数据而不使用原子操作,你需要使用锁,而这是一个方便的选择。与其他锁不同,在持有 VM 锁的情况下,你可以分配 Ruby 管理的内存。当你获取 VM 锁时,在临界区内有一些可以做和不能做的事情。

你可以(只要在获取 VM 锁之前没有持有其他锁)

你不能

在内部,VM 锁是 vm->ractor.sync.lock

你需要在一个 Ruby 线程上才能获取 VM 锁。你也不能在任何可能在 GC 扫描期间被调用的函数中获取它,因为 MMTK 在另一个线程上进行扫描,而你需要一个有效的 ec 才能获取锁。出于同样的原因(以及其他原因),你也不能从定时器线程获取它。

其他锁

所有非 VM 锁的原生锁对于临界区内的操作有一套更严格的规则。原生锁是指使用 rb_native_mutex_lock 的任何东西。一些重要的锁包括 interrupt_lock、Ractor 调度锁(保护全局调度数据结构)、线程调度锁(每个 Ractor 本地,保护每个 Ractor 的调度数据结构)和 Ractor 锁(每个 Ractor 本地,保护 Ractor 数据结构)。

当你获取这些锁之一时,

你可以

你不能

VM 锁与 GVL 之间的区别

VM 锁是源代码中的一个特定锁。只有一个 VM 锁。而 GVL 更多的是锁的组合。当一个 Ruby 线程即将运行或正在运行时,“获取”GVL。由于在不同的 Ractor 中,许多 Ruby 线程可以同时运行,因此存在多个 GVL(每个 SNT 一个 + 主 Ractor 一个)。它不再可以被视为“全局 VM 锁”,就像在 Ractor 出现之前那样。

VM 屏障

有时,仅获取 VM 锁是不够的,你需要保证所有 Ractor 都已停止。例如,在运行 GC 时就会发生这种情况。要创建屏障,你需要获取 VM 锁并调用 rb_vm_barrier()。在持有 VM 锁期间,没有其他 Ractor 会运行。它不常使用,因为获取屏障会显著降低 Ractor 的性能,但了解它很有用,有时它是唯一的解决方案。

锁顺序

最好不要在同一线程上一次持有超过 2 个锁。锁定多个锁可能会导致死锁,因此请谨慎操作。当一次锁定多个锁时,请遵循跨程序一致的顺序,否则可能会导致死锁。以下是一些重要锁的顺序:

这些顺序可能会发生变化,因此如果不确定,请检查源代码。在此基础上

Ruby Interrupt 处理

当 VM 运行 Ruby 代码时,Ruby 线程会间歇性地检查 Ruby 级别的中断。这些软件中断用于 Ruby 中的各种事物,它们可以由其他 Ruby 线程或定时器线程设置。

这不是一个完整的列表。

在向 Ruby 线程发送中断时,Ruby 线程可能会被阻塞。例如,它可能正处于 TCPSocket#read 调用中间。如果是这样,接收线程的 ubf(解阻塞函数)将从发送中断的线程(Ruby 线程或定时器线程)调用。每个 Ruby 线程都有一个 ubf,在进入阻塞操作时设置,在返回后取消设置。默认情况下,此 ubf 函数会向接收线程发送 SIGVTALRM 以尝试从内核中解除阻塞它,以便它可以检查中断。还有其他 ubfs 与系统调用无关,例如在调用 Ractor#joinsleep 时。所有 ubfs 都在持有 interrupt_lock 的情况下调用,因此在使用 ubfs 内的锁时要考虑这一点。

请记住,ubfs 可以从定时器线程调用,因此你不能在其中假定存在 ecec(执行上下文)仅在 Ruby 线程上设置。

定时器 Thread

定时器线程有几个功能。它们是: