starting from Rails 4,默认情况下,所有内容都必须在线程环境中运行。这意味着我们编写的所有代码 AND ALL 我们使用的宝石必须是threadsafe
所以,我对此几乎没有问题:
@result ||= some_method
?Hash
等线程安全?GVL
/GIL
这意味着除了IO
之外一次只能运行1个红宝石线程,线程安全变化是否会影响我们? 答案 0 :(得分:103)
没有核心数据结构是线程安全的。我所知道的唯一一个附带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
是线程安全的,因为它是一个操作(它有很多操作,如果我试图描述它为什么"线程安全&我可能会遇到麻烦#34;详细说明,但最终你不会得到来自作业的不一致结果)。 @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
常量不再具有相同的值。常量只是它们引用的常量,它们不保证它们引用的数据结构的恒定性。试想一下,如果这个代码同时运行会发生什么。
如果你避免共享的可变状态(例如,多个线程访问的对象中的实例变量,像多个线程访问的哈希和数组这样的数据结构)线程安全性就不那么难了。尽量减少同时访问的应用程序部分,并将工作重点放在那里。 IIRC,在Rails应用程序中,为每个请求创建一个新的控制器对象,因此它只会被单个线程使用,对于从该控制器创建的任何模型对象也是如此。但是,Rails还鼓励使用全局变量(User.find(...)
使用全局变量User
,您可能认为它只是一个类,它是一个类,但它也是一个全局命名空间变量),其中一些是安全的,因为它们是只读的,但有时你会在这些全局变量中保存,因为它很方便。当您使用全球可访问的任何内容时要非常小心。
现在可以在线程环境中运行Rails很长一段时间了,所以如果不是Rails专家,我仍然会说你不必担心线程安全谈到Rails本身。您仍然可以通过执行上面提到的一些操作来创建不具有线程安全性的Rails应用程序。当它出现时,其他宝石认为它们不是线程安全的,除非他们说它们是,并且如果他们说他们认为它们不是,并查看他们的代码(但只是因为你看到它们像{{{ 1}}并不意味着它们不是线程安全的,在正确的上下文中做完全合法的事情 - 你应该寻找全局变量中的可变状态,它如何处理传递给它的可变对象方法,尤其是它如何处理选项哈希)。
最后,线程不安全是一个传递属性。任何使用非线程安全的东西本身都不是线程安全的。
答案 1 :(得分:10)
除了Theo的答案之外,如果您要切换到config.threadsafe,我会在Rails中添加一些问题区域,以备不时寻找!
类变量:
@@i_exist_across_threads
<强> ENV 强>:
ENV['DONT_CHANGE_ME']
<强>线程强>:
Thread.start
答案 2 :(得分:8)
从Rails 4开始,默认情况下,所有内容都必须在线程环境中运行
这不是100%正确。默认情况下,线程安全Rails是打开的。如果您在像Passenger(社区)或Unicorn这样的多进程应用服务器上部署,则根本没有区别。如果您在Puma或Passenger Enterprise等多线程环境中部署,则此更改仅与您有关。 4.0
过去如果你想在多线程应用服务器上部署,你必须打开 config.threadsafe ,这是现在的默认值,因为它所做的一切都没有效果或者也没有应用到一个进程中运行的Rails应用程序(Prooflink)。
但是如果你确实想要所有Rails 4 streaming的好处以及多线程部署的其他实时内容 那么也许你会发现this文章很有趣。正如@Theo悲伤,对于Rails应用程序,您实际上只需要在请求期间省略变异静态。虽然这是一个简单的做法,但遗憾的是,对于您找到的每一颗宝石,您无法确定这一点。据我所知,来自JRuby项目的Charles Oliver Nutter在this播客中有一些关于它的提示。
如果你想编写一个纯粹的并发Ruby编程,你需要一些可以被多个线程访问的数据结构,你可能会发现thread_safe gem很有用。