如何知道 ruby​​ 中啥不是线程安全的?

Posted

技术标签:

【中文标题】如何知道 ruby​​ 中啥不是线程安全的?【英文标题】:how to know what is NOT thread-safe in ruby?如何知道 ruby​​ 中什么不是线程安全的? 【发布时间】:2013-02-17 12:35:32 【问题描述】:

starting from Rails 4,默认情况下,一切都必须在线程环境中运行。这意味着我们编写的所有代码 AND ALL 我们使用的 gem 必须是 threadsafe

所以,我对此有几个问题:

    在 ruby​​/rails 中什么不是线程安全的? Vs 什么是 ruby​​/rails 中的线程安全? 是否有一个 已知为线程安全或反之亦然的 gem 列表? 是否有非线程安全的常见代码模式列表@result ||= some_methodHash等ruby lang核心中的数据结构是线程安全的吗? 在 MRI 上,GVL/GIL 表示一次只能运行 1 个 ruby​​ 线程,除了IO,线程安全更改对我们有影响吗?

【问题讨论】:

您确定所有代码和所有 gem 都必须是线程安全的吗?发行说明说的是 Rails 本身将是线程安全的,而不是与它一起使用的所有其他东西都必须是 多线程测试将是最糟糕的线程安全风险。当您必须更改测试用例周围的环境变量的值时,您立即不是线程安全的。你会如何解决这个问题?是的,所有的 gem 都必须是线程安全的。 【参考方案1】:

没有一个核心数据结构是线程安全的。据我所知,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 常量不再具有相同的值。常量仅在它们引用的内容上是恒定的,它们不保证它们所引用的数据结构的恒定性。想想如果这段代码同时运行会发生什么。

如果您避免共享可变状态(例如,多线程访问的对象中的实例变量、多线程访问的哈希和数组等数据结构),线程安全就不是那么难了。尽量减少同时访问的应用程序部分,并将精力集中在那里。 IIRC,在 Rails 应用程序中,为每个请求创建一个新的控制器对象,因此它只会被单个线程使用,您从该控制器创建的任何模型对象也是如此。但是,Rails 也鼓励使用全局变量(User.find(...) 使用全局变量User,你可能认为它只是一个类,它是一个类,但它也是全局变量的命名空间),一些其中一些是安全的,因为它们是只读的,但有时您会将内容保存在这些全局变量中,因为它很方便。使用可全局访问的任何内容时要非常小心。

现在可以在线程环境中运行 Rails 已经有一段时间了,所以如果不是 Rails 专家,我仍然会说你不必担心 Rails 本身的线程安全.通过执行我上面提到的一些事情,您仍然可以创建不是线程安全的 Rails 应用程序。当涉及到其他 gem 时,除非他们说它们是线程安全的,否则如果他们说它们是假设它们不是,则假设它们不是线程安全的,并查看它们的代码(但只是因为你看到它们像 @987654341 @ 并不意味着它们不是线程安全的,这是在正确的上下文中做的完全合法的事情——你应该在全局变量中寻找可变状态,它如何处理传递给它的方法的可变对象,尤其是如何它处理选项哈希)。

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

【讨论】:

很好的答案。考虑到典型的 Rails 应用程序是多进程的(如您所描述,许多不同的用户访问同一个应用程序),我想知道线程对并发模型的 边际风险 是什么......换句话说,如果您已经通过进程处理一些并发,那么在线程模式下运行还有多少“危险”? @Theo 非常感谢。那些不断的东西是一个大炸弹。它甚至不是过程安全的。如果常量在一个请求中被更改,它会导致后面的请求即使在单个线程中也能看到更改的常量。 Ruby 常量很奇怪 STANDARD_OPTIONS = ....freeze 提高浅突变 非常棒的答案 “如果你写像@n = 0; 3.times Thread.start 100.times @n += 1 [...]这样的代码,之后共享变量的值是不确定的。” - 你知道这是否不同Ruby 的版本?例如,在 1.8 上运行您的代码会给出不同的 @n 值,但在 1.9 及更高版本上,它似乎一致地给出等于 300 的 @n【参考方案2】:

除了 Theo 的回答之外,如果您要切换到 config.threadsafe,我会在 Rails 中添加几个需要特别注意的问题区域!

类变量

@@i_exist_across_threads

环境

ENV['DONT_CHANGE_ME']

线程

Thread.start

【讨论】:

【参考方案3】:

从 Rails 4 开始,默认情况下一切都必须在线程环境中运行

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

过去,如果您想在多线程应用服务器上部署,您必须打开 config.threadsafe,这是现在的默认设置,因为它所做的一切要么没有效果,要么也被应用到在单个进程中运行的 Rails 应用程序 (Prooflink)。

但是,如果您确实想要 Rails 4 streaming 的所有好处和多线程部署的其他实时内容 那么也许你会发现this 的文章很有趣。正如@Theo 可悲的那样,对于 Rails 应用程序,您实际上只需要在请求期间省略变异静态状态。虽然这是一个简单的做法,但不幸的是,您无法确定您找到的每颗宝石都遵循这一点。据我所知,来自 JRuby 项目的 Charles Oliver Nutter 在this 播客中有一些关于它的提示。

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

【讨论】:

以上是关于如何知道 ruby​​ 中啥不是线程安全的?的主要内容,如果未能解决你的问题,请参考以下文章

java中啥叫做线程?啥叫多线程?多线程的特点是啥?

不会吧,你连Java 多线程线程安全都还没搞明白,难怪你面试总不过

29.安全集合

29.安全集合

检查库/API是不是是线程安全的通用方法

Linux线程安全篇