没有一个核心数据结构是线程安全的。据我所知,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
, is not,也没有其他速记操作+赋值。我犯过很多次的一个错误是 write return unless @started; @started = true
,这根本不是线程安全的。
我不知道任何关于 Ruby 的线程安全和非线程安全语句的权威列表,但是有一个简单的经验法则:如果一个表达式只执行一个(无副作用)操作,它可能是线程安全的。例如:如果方法没有副作用(因为在 Ruby 中几乎任何东西都是方法调用,在很多情况下甚至是赋值,这也适用于其他示例a + b
)a = b
。在这种情况下,副作用意味着改变状态的事物。没有副作用。a.foo(b)
foo
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
常量不再具有相同的值。常量仅在它们引用的内容上是恒定的,它们不保证它们所引用的数据结构的恒定性。想想如果这段代码同时运行会发生什么。
如果您避免共享可变状态(例如,由多个线程访问的对象中的实例变量,由多个线程访问的哈希和数组等数据结构)线程安全并不是那么难。尽量减少同时访问的应用程序部分,并将精力集中在那里。IIRC,在 Rails 应用程序中,为每个请求创建一个新的控制器对象,因此它只会被单个线程使用,您从该控制器创建的任何模型对象也是如此。但是,Rails 也鼓励使用全局变量(User.find(...)
使用全局变量User
,你可能认为它只是一个类,它是一个类,但它也是全局变量的命名空间),其中一些是安全的,因为它们是只读的,但有时你将东西保存在这些全局变量中,因为它很方便。使用可全局访问的任何内容时要非常小心。
在线程环境中运行 Rails 已经有一段时间了,所以如果不是 Rails 专家,我仍然会说你不必担心 Rails 本身的线程安全。通过执行我上面提到的一些事情,您仍然可以创建不是线程安全的 Rails 应用程序。当涉及到其他 gem 时,除非他们说它们是线程安全的,否则如果他们说它们是假设它们不是,则假设它们不是线程安全的,并查看它们的代码(但只是因为你看到它们像这样@n ||= 1
并不意味着它们不是线程安全的,这是在正确的上下文中做的完全合法的事情——你应该寻找诸如全局变量中的可变状态,它如何处理传递给它的方法的可变对象,尤其是它如何处理选项哈希)。
最后,线程不安全是一个传递属性。任何使用非线程安全的东西本身就不是线程安全的。