如何知道什么是不是线程安全的ruby?
从Rails 4开始 ,所有的东西都必须在线程环境下默认运行。 这意味着我们所写的所有代码,我们所使用的所有gem都必须是threadsafe
所以,我对此有几个问题:
- ruby / rails中不是线程安全的? Vs什么是ruby / rails中的线程安全?
- 是否有一个已知是线程安全的gem列表,反之亦然?
- 有没有线程安全的代码常见模式列表
@result ||= some_method
? - ruby中的核心数据结构如
Hash
等线程安全吗? - 在MRI上,
GVL
/GIL
只有一个ruby线可以一次运行,除了IO
之外,线程安全变更对我们有什么影响?
核心数据结构都不是线程安全的。 我知道的唯一一个与Ruby相关的是标准库中的队列实现( require 'thread'; q = Queue.new
)。
MRI的GIL并不能解救我们的线程安全问题。 它只能确保两个线程不能同时运行Ruby代码,即在两个不同的CPU上同时运行。 线程仍然可以在代码中的任何时刻暂停和恢复。 如果你写的代码像@ n @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
@n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
例如,从多个线程中variables共享variables,之后共享variables的值不是确定性的。 GIL或多或less是一个单核心系统的模拟,它并没有改变编写正确并发程序的基本问题。
即使MRI像Node.js一样是单线程的,你仍然需要考虑并发性。 增量variables的例子可以正常工作,但是你仍然可以获得竞争条件,以非确定性顺序发生事件,并且一个callback会破坏另一个的结果。 单线程asynchronous系统比较容易推理,但并不是没有并发问题。 试想一下有多个用户的应用程序:如果两个用户或多或less地同时在堆栈溢出post上点击编辑,花费一些时间编辑post然后点击保存,之后他们的更改将被第三个用户看到阅读同一篇文章?
在Ruby中,与其他大多数并发运行时一样,超过一个操作的任何操作都不是线程安全的。 @n += 1
不是线程安全的,因为它是多个操作。 @n = 1
是线程安全的,因为它是一个操作(这是很多操作,如果我试图详细描述为什么它是“线程安全的”,我可能会陷入困境,但最终你不会得到来自任务的不一致的结果)。 @n ||= 1
,不是,也没有其他的简写操作+赋值也是。 我多次犯的一个错误是写return unless @started; @started = true
return unless @started; @started = true
,这是不是线程安全的。
我不知道任何Ruby的线程安全和非线程安全语句的权威列表,但有一个简单的经验法则:如果一个expression式只有一个(免费的副作用)操作,它可能是线程安全的。 例如: a + b
可以, a = b
也可以,而a.foo(b)
也可以, 如果foo
方法是免费的 (因为Ruby中的任何东西都是方法调用,甚至赋值很多情况下,这也适用于其他例子)。 在这种情况下的副作用意味着改变状态的事物。 def foo(x); @x = x; end
def foo(x); @x = x; end
不是副作用的自由。
在Ruby中编写线程安全代码最困难的事情之一就是所有的核心数据结构,包括数组,哈希和string都是可变的。 意外泄漏你的状态是很容易的,当这件事情是可变的,事情会变得非常糟糕。 考虑下面的代码:
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
存取器泄漏。 从封装的angular度来看,除了存在问题以外,还会出现一jar并发蠕虫。 也许有人把这个数组传递到别的地方,然后这个代码又认为它现在拥有这个数组,并且可以做任何想做的事情。
另一个经典的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
常量不再具有相同的值。 常量只是它们所引用的常量,它们并不能保证它们引用的数据结构的稳定性。 试想如果这个代码同时运行会发生什么。
如果避免共享可变状态(例如,多个线程访问的对象中的实例variables,像多个线程访问的哈希和数组等数据结构),线程安全并不难。 尽量减less同时访问的应用程序的部分,并集中精力。 IIRC,在一个Rails应用程序中,为每个请求创build一个新的控制器对象,所以它只能被单个线程使用,并且对于从该控制器创build的所有模型对象也是如此。 不过,Rails也鼓励使用全局variables( User.find(...)
使用全局variablesUser
,你可能认为它只是一个类,它是一个类,但它也是全局variables的命名空间),其中一些是安全的,因为它们是只读的,但是有时候您将这些全局variables保存在这些全局variables中,因为它很方便。 使用任何可以全球访问的东西时要非常小心。
在线程化环境中运行Rails已经有一段时间了,所以不需要Rails专家,我仍然可以说Rails本身并不需要担心线程的安全问题。 您仍然可以通过执行上面提到的一些事情来创build不是线程安全的Rails应用程序。 当谈到其他的gem时,假设他们不是线程安全的,除非他们说他们是,而且如果他们说他们认为他们不是,并且浏览他们的代码(只是因为你看到他们像@n ||= 1
并不意味着它们不是线程安全的,在正确的上下文中这是一个完全合法的事情 – 你应该在全局variables中寻找像可变状态的东西,它如何处理传递给它的方法的可变对象,特别是它如何处理选项哈希)。
最后,线程不安全是一个传递属性。 任何使用不是线程安全的东西本身都不是线程安全的。
除了Theo的回答之外,如果您切换到config.threadsafe,我会添加一些问题区域来专门查找Rails。
-
类variables :
@@i_exist_across_threads
-
ENV :
ENV['DONT_CHANGE_ME']
-
主题 :
Thread.start
从Rails 4开始,所有的东西都必须在线程环境下默认运行
这不是100%正确的。 Threadsafe Rails只是在默认情况下。 如果您仍然部署在乘客(社区)或独angular兽等多进程应用程序服务器上,则完全没有任何区别。 如果您在Puma或Passenger Enterprise> 4.0等multithreading环境中进行部署,此更改只涉及到您
在过去,如果你想部署在一个multithreading的应用服务器上,你必须打开现在默认的config.threadsafe ,因为它没有任何效果,或者也适用于在一个进程中运行的rails应用程序( Prooflink )。
但是,如果你想要所有的multithreading部署的好处和其他实时的东西,那么也许你会发现这篇文章很有趣。 作为@Theo悲伤,对于一个Rails应用程序,你只需要在请求期间省略变异的静态。 虽然这是一个简单的做法,不可思议的是你不能确定你find的每一个gem。 据我记得,Jruby项目的Charles Oliver Nutter在这个播客中有一些提示。
如果你想写一个纯粹的并发ruby编程,在那里你需要一些数据结构可以被多个线程访问,你可能会发现thread_safe gem usefull