Ruby 代码线程安全的一些编写原则

避免修改全局共享的对象

非必要的情况下尽量避免修改全局共享的对象,包括 $ 开头的全局变量、单实例对象、AST、类变量/方法等。

下面的写法是线程安全的,因为它没有修改全局的状态:

class RainyCloudFactory
  def self.generate
    cloud = Cloud.new
    cloud.rain!

    cloud
  end
end

下面的写法不是线程安全的,因为它修改了全局的状态:

class RainyCloudFactory
  def self.generate
    cloud = Cloud.new
    @@clouds << cloud

    cloud
  end
end

但是,如果非用全局共享的对象不可,也不是不能用,只要保证线程安全即可,例如:

require 'thread'

class Counter
  def initialize
    @counter = 0
    @mutex = Mutex.new
  end

  def increment
    @mutex.synchronize do
      @counter += 1
    end
  end
end

$counter = Counter.new

AST

上面提到的 AST 指的是程序的抽象语法树里的指令,Ruby 作为一门动态语言,是允许在运行时修改它的。所有的线程共享一份 AST, 因此在多线程环境下,同样存在线程安全的问题——虽然很罕见。kaminari 这个 Gem 曾经就出过这样的一个问题:https://github.com/kaminari/kaminari/issues/214

代码里动态地定义了一个方法,然后用 alias_method 为其创建别名,接着又删除了初始定义的那个方法。在多线程环境下,就会出现这样的情况:线程 A 定义了一个方法,并且为其创建了别名。接着线程 B 也定义了这个方法,覆盖了线程 A 定义的方法。然后线程 A 删除了该方法。线程 B 再为它创建别名时,就抛出了 NoMethodError 异常。

所以,为了线程安全,应该避免在运行时修改 AST. 这里的“运行时”,指的是应用程序运行的过程中。或者换句话说,AST 的修改应该在程序的启动过程中完成

创建更多的对象,而不要共享同一个

有时候我们免不了还是需要使用全局对象,例如数据库连接。这时候有两种办法解决线程安全问题。

Thread-locals

Thread-locals 的方案,是让对象仅在当前线程里全局可见。下面是个例子:

# 把这样的代码:
$redis = Redis.new
# 用这种方式替代:
Thread.current[:redis] = Redis.new

Thread-locals 的方案会为每个线程创建一个对应实例。但是当线程数很大的时候,资源池可能是一个更好的方案。

资源池

假设有 N 个线程需要连接 Redis, 连接池里有 M 个连接可供使用,M < N. 这种情况下资源池仍然能够保证线程安全。

一个资源池通常维护着一定数量的资源供多个线程使用。当一个线程需要使用该资源(比如 Redis 连接)时,资源池会取出一个资源供其使用,并在使用完毕后将该资源放回池中以待下一个线程使用。这样保证了线程安全。

避免延迟加载

在 3.0 版本之前,Ruby on Rails 中的一个习惯用法是在运行时加载常量,用的是类似 Ruby 的 autoload 的方法。但这不是个好的做法,因为 MRI Ruby 的 autoload 并不是线程安全的。虽然在现在的 JRuby 里,autoload 是线程安全的,但最好的做法还是在派生 worker 线程之前预先加载需要的常量。

优先使用(线程安全的)数据结构而不是 Mutex

Mutex 不容易正确地使用。在使用 Mutex 的时候你要考虑许多问题:

  • 这个 Mutex 的粒度应该有多大?
  • 这里会不会发生死锁?
  • 我应该为每个对象创建一个 Mutex 还是使用全局的 Mutex 对象?

以上问题只是需要考虑的其中一部分。当然对于熟练掌握 Mutex 用法的程序员来说,这些问题不难回答。

但使用数据结构(例如 Queue)可以免掉这些考量。与其担心这些问题,还不如干脆不用 Mutex.

另外,这里还有一些来自 JRuby wiki 的关于编写并发代码的“安全路径”:

  • 不要编写并发代码,除非无法避免;
  • 如果你非得编写并发代码不可,不要在线程间共享数据;
  • 如果非得在线程间共享数据不可,不要共享可变数据;
  • 如果非共享可变数据不可,在访问这些数据时做同步处理。
1