核心数据结构都不是线程安全的。我所知道的唯一一个带有Ruby的是标准库中的队列实现 (require 'thread'; q = Queue.new
)。
MRI的GIL并不能保护我们免受线程安全问题的影响。它只能确保两个线程不能同时运行Ruby代码,即在两个不同的CPU上完全相同的时间运行。线程仍然可以在您的代码中的任何时刻暂停和恢复。如果您编写像 @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
这样的代码,例如从多个线程更改共享变量,则共享变量之后的值是不确定的。 GIL或多或少是单核系统的模拟,它不会改变编写正确并发程序的基本问题。
即使MRI像Node.js一样是单线程的,你仍然需要考虑并发性。增加变量的示例可以正常工作,但仍然可能出现竞态条件,在非确定性顺序中发生某些事情,一个回调会覆盖另一个回调的结果。单线程异步系统更容易理解,但它们不免于并发问题。只需想象具有多个用户的应用程序:如果两个用户在几乎同时点击编辑Stack Overflow帖子,花费一些时间编辑帖子,然后点击保存,当第三个用户稍后阅读相同帖子时,谁的更改将被看到? 在Ruby中,与大多数其他并发运行时一样,任何超过一个操作的操作都不是线程安全的。`@n += 1`不是线程安全的,因为它是多个操作。`@n = 1`是线程安全的,因为它是一个操作(在幕后有很多操作,如果我试图详细描述为什么它是“线程安全的”,我可能会陷入麻烦,但最终您将不会从分配中获得不一致的结果)。`@n ||= 1`也不是线程安全的,也没有其他简写操作+赋值。我犯过的一个错误是编写`return unless @started; @started = true`,这根本不是线程安全的。
我不知道有关于Ruby线程安全和非线程安全语句的官方列表,但是有一个简单的经验法则:如果一个表达式只执行一个(无副作用的)操作,那么它很可能是线程安全的。例如:
a + b
是可以的,
a = b
也可以,
a.foo(b)
也可以,
如果方法foo
没有副作用(由于在Ruby中几乎任何东西都是方法调用,即使在许多情况下也适用于其他示例)。在这个上下文中,副作用指的是改变状态的事情。
def foo(x); @x = x; end
不是无副作用的。
在Ruby中编写线程安全代码最困难的一点是,所有核心数据结构,包括数组、哈希和字符串,都是可变的。很容易意外泄漏一部分状态,而当这部分状态是可变的时,事情会变得非常混乱。考虑以下代码:
class Thing
attr_reader :stuff
def initialize(initial_stuff)
@stuff = initial_stuff
@state_lock = Mutex.new
end
def add(item)
@state_lock.synchronize do
@stuff << item
end
end
end
这个类的实例可以在线程之间共享,并且它们可以安全地向其中添加内容,但是存在并发错误(这不是唯一的错误):对象的内部状态通过
stuff
访问器泄漏。除了从封装角度来看有问题外,它还打开了一个并发蠕虫的罐子。也许有人会获取该数组并将其传递给其他地方,然后那段代码又认为它现在拥有该数组并可以随意使用它。
另一个经典的Ruby示例是:
STANDARD_OPTIONS = {:color => 'red', :count => 10}
def find_stuff
@some_service.load_things('stuff', STANDARD_OPTIONS)
end
find_stuff
第一次使用时工作正常,但第二次返回其他内容。为什么?load_things
方法碰巧认为它拥有传递给它的选项哈希,并执行color = options.delete(:color)
。现在STANDARD_OPTIONS
常量不再具有相同的值。常量只在引用的内容上是恒定的,它们不能保证所引用的数据结构的恒定性。想象一下如果此代码并发运行会发生什么。
如果您避免使用共享的可变状态(例如多个线程访问的对象中的实例变量、哈希和数组等数据结构),那么线程安全并不难实现。尽量减少应用程序中被并发访问的部分,并将重点放在这些部分上。据我所知,在Rails应用程序中,每个请求都会创建一个新的控制器对象,因此它只会被单个线程使用,从该控制器创建的任何模型对象也是如此。但是,Rails也鼓励使用全局变量(
User.find(...)
使用全局变量
User
,您可能认为它只是一个类,它确实是一个类,但也是全局变量的命名空间),其中一些是安全的,因为它们只读,但有时您会在这些全局变量中保存一些东西,因为这很方便。在使用任何全局访问的内容时要非常小心。
很长一段时间以来,运行 Rails 在线程环境中已经成为可能,所以即使不是 Rails 专家,我仍然可以说,在涉及到 Rails 本身时,您不必担心线程安全性。但是,您仍然可以通过执行我上面提到的某些操作来创建不支持线程的 Rails 应用程序。对于其他 gem,假设它们不是线程安全的,除非它们声明了自己是线程安全的,如果它们声明了自己是线程安全的,则假定它们不是线程安全的,并查看它们的代码(但只是因为您看到它们执行像
@n ||= 1
这样的操作并不意味着它们不是线程安全的,在正确的上下文中这是完全合法的事情 - 相反,您应该寻找全局变量中的可变状态、它如何处理传递给其方法的可变对象,以及特别是它如何处理选项哈希)。
最后,线程不安全是一种传递属性。使用不支持线程的任何内容本身也不是线程安全的。