如何知道红宝石不是线程安全的?


93

从Rails 4开始,默认情况下所有内容都必须在线程环境中运行。这意味着所有我们写的代码 所有我们使用需要是宝石threadsafe

因此,我对此没有几个问题:

  1. 在ruby / rails中,什么不是线程安全的?VS什么是Ruby / Rails中的线程安全?
  2. 是否有宝石的列表称为是线程安全的,反之亦然?
  3. 是否有不是线程安全示例的常见代码模式列表@result ||= some_method
  4. ruby lang核心中的数据结构(例如Hashetc线程)是否安全?
  5. 在MRI上,一个GVL/GIL表示一次只能运行1条红宝石线程,除了IO,线程安全更改对我们有影响吗?

2
您确定所有代码和所有gem都是线程安全的吗?发行说明说的是,Rails本身将是线程安全的,而不是与它一起使用的其他所有东西都将成为
现实

多线程测试将是最糟糕的线程安全风险。当您必须在测试用例周围更改环境变量的值时,您立即就不是线程安全的。您将如何解决?是的,所有的宝石都必须是线程安全的。
Lukas Oberhuber

Answers:


110

没有一个核心数据结构是线程安全的。我所知道的Ruby附带的唯一一个就是标准库(require 'thread'; q = Queue.new)中的队列实现。

MRI的GIL不能使我们摆脱线程安全问题。它仅确保两个线程不能同时运行Ruby代码,即,不能在同一时间在两个不同的CPU上运行Ruby代码。在代码中的任何时候,线程仍然可以暂停和恢复。如果您编写的代码(@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是ok,a = b也可以,并且a.foo(b)可以的(因为Ruby中的几乎所有东西都是方法调用,即使在很多情况下也是赋值,其他示例也是如此)。在这种情况下,副作用是指改变状态的事物。是不是没有副作用。foodef 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方法碰巧认为它拥有传递给它的options哈希,并且做到了color = options.delete(:color)。现在,该STANDARD_OPTIONS常数不再具有相同的值。常量仅在它们引用的内容中是常量,它们不能保证所引用的数据结构的恒定性。试想一下,如果同时运行此代码会发生什么。

如果避免共享的可变状态(例如,多个线程访问的对象中的实例变量,散列和多个线程访问的数据结构等数据结构),则线程安全性并不难。尝试最小化应用程序中并发访问的部分,并集中精力进行。IIRC在Rails应用程序中,将为每个请求创建一个新的控制器对象,因此它将仅由单个线程使用,对于从该控制器创建的任何模型对象也是如此。但是,Rails还鼓励使用全局变量(User.find(...)使用全局变量User,您可能会认为它只是一个类,并且它是一个类,但它也是全局变量的命名空间),其中一些是安全的,因为它们是只读的,但是有时您将它们保存在这些全局变量中,因为很方便。使用任何可全局访问的内容时,请务必小心。

现在已经有可能在线程环境中运行Rails一段时间了,因此,如果没有成为Rails专家,我什至会说,您不必担心Rails本身的线程安全性。通过执行我上面提到的一些事情,您仍然可以创建不是线程安全的Rails应用程序。当其他gem出现时,除非它们声称它们是线程安全的,否则它们就假定它们不是线程安全的;如果它们表示它们不是,则认为它们不是线程安全的,并仔细阅读其代码(但仅仅是因为您看到它们运行类似@n ||= 1 这并不意味着它们不是线程安全的,这是在正确的上下文中做的完全合法的事情-相反,您应该在全局变量中查找诸如可变状态,如何处理传递给其方法的可变对象的事情,尤其是它如何处理选项哈希)。

最后,线程不安全是可传递的属性。任何使用非线程安全的东西本身都不是线程安全的。


好答案。考虑到一个典型的Rails应用程序是多进程的(就像您描述的那样,许多不同的用户访问同一个应用程序),我想知道并发模型中线程的边际风险是什么……换句话说,有多少“危险”如果您已经通过进程处理了一些并发,它是否可以在线程模式下运行?
gingerlime 2013年

2
@Theo非常感谢。那恒定的东西是一颗巨大的炸弹。它甚至都不安全。如果在一个请求中更改了常量,则即使在单个线程中,也将导致以后的请求看到更改后的常量。Ruby常量很奇怪
残酷的

5
不要STANDARD_OPTIONS = {...}.freeze提高对浅突变
glebm

真正的好答案
Cheyne

3
“如果您编写类似@n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...]的代码,则共享变量的值随后将不确定。”-您知道Ruby版本之间是否有所不同?例如,在1.8上运行的代码给出了不同的值@n,但在1.9以后似乎一直给予@n等于300
user200783

10

除了Theo的答案之外,如果您要切换到config.threadsafe,我还要添加一些需要在Rails中注意的问题区域!

  • 类变量

    @@i_exist_across_threads

  • ENV

    ENV['DONT_CHANGE_ME']

  • 线程数

    Thread.start


9

从Rails 4开始,默认情况下所有内容都必须在线程环境中运行

这不是100%正确的。默认情况下,线程安全Rails仅处于启用状态。如果您在“乘客(社区)”或“独角兽”等多进程应用服务器上进行部署,则完全没有区别。如果您在Puma或Passenger Enterprise> 4.0之类的多线程环境中进行部署,则此更改仅与您有关

过去,如果要在多线程应用服务器上进行部署,则必须打开config.threadsafe,这是现在的默认设置,因为它所做的所有操作都没有效果,或者也已应用于在单个进程中运行的Rails应用程序(Prooflink)。

但是,如果你希望所有的Rails 4 的好处和多线程部署的其他实时琐事,那么也许你会觉得文章有趣。不幸的是,对于The Rails应用程序@Theo来说,实际上您只需要在请求期间省略静态变化即可。虽然这是一个简单的实践,但是不幸的是,您不能为找到的每一颗宝石都确定这一点。据我记得,来自JRuby项目的Charles Oliver Nutter在播客中有关于它的一些技巧。

而且,如果您想编写纯并发的Ruby编程,那么您将需要一些可以被多个线程访问的数据结构,您可能会发现thread_safe gem很有用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.