1

在 JRuby 中编写线程安全代码时,希望确保我们使用了正确的同步(并且仅是必要的);具体来说,在 Puma 实例化的 Rails 应用程序中。

更新:广泛重新编辑了这个问题,非常清楚并使用我们正在实施的最新代码。此代码使用atomic@headius (Charles Nutter) 为JRuby 编写的gem,但不确定它是否完全必要,或者以何种方式需要,对于我们在这里尝试做的事情。

这就是我们所得到的,这是过度杀伤(意思是,我们是否过度/超级工程化),或者可能不正确?

我们的宝石.rb:

require 'atomic'  # gem from @headius

SUPPORTED_SERVICES = %w(serviceABC anotherSvc andSoOnSvc).freeze

module Foo

  def self.included(cls)
    cls.extend(ClassMethods)
    cls.send :__setup
  end

  module ClassMethods
    def get(service_name, method_name, *args)
      __cached_client(service_name).send(method_name.to_sym, *args)
      # we also capture exceptions here, but leaving those out for brevity
    end

    private

    def __client(service_name)
      # obtain and return a client handle for the given service_name
      # we definitely want to cache the value returned from this method
      # **AND**
      # it is a requirement that this method ONLY be called *once PER service_name*.
    end

    def __cached_client(service_name)
      @@_clients.value[service_name]
    end

    def __setup
      @@_clients = Atomic.new({})
      @@_clients.update do |current_service|
        SUPPORTED_SERVICES.inject(Atomic.new({}).value) do |memo, service_name|
          if current_services[service_name]
            current_services[service_name]
          else
            memo.merge({service_name => __client(service_name)})
          end
        end
      end
    end
  end
end

客户端.rb:

require 'ourgem'

class GetStuffFromServiceABC
  include Foo

  def self.get_some_stuff
    result = get('serviceABC', 'method_bar', 'arg1', 'arg2', 'arg3')
    puts result
  end
end

上面的总结:我们有@@_clients(一个可变的类变量,持有一个客户端的哈希),我们只想为所有可用的服务填充一次,这些服务以 service_name 为键。

由于哈希在类变量中(因此是线程安全的?),我们是否保证__client每个服务名称的调用不会多次运行(即使 Puma 使用此类实例化多个线程以服务来自不同的所有请求用户)?如果类变量是线程安全的(以这种方式),那么也许Atomic.new({})是不必要的?

另外,我们应该改用 anAtomic.new(ThreadSafe::Hash)吗?或者,这不是必需的吗?

如果不需要(意思是:你认为我们至少Atomic.new需要 s,也许还需要s ThreadSafe::Hash),那么为什么第二个(或第三个等)线程不能在 s 之间中断,而Atomic.new(nil)每个线程中的 s 将每个创建两个(单独的)对象?@@_clients.update do ...Atomic.new

感谢您提供任何线程安全建议,我们在 SO 上没有看到任何直接解决此问题的问题。

4

1 回答 1

3

在我尝试解决您在这里提出的问题之前,只是一个友好的建议:

这个问题和随附的代码强烈建议您(还)没有牢牢掌握编写多线程代码所涉及的问题。我鼓励您在决定编写多线程应用程序以供生产使用之前三思而后行。为什么你真的想使用 Puma?是为了表演吗?您的应用程序会同时处理许多长时间运行的、受 I/O 限制的请求(例如上传/下载大文件)吗?或者(像许多应用程序一样)它会主要处理短的、受 CPU 限制的请求吗?

如果答案是“short/CPU-bound”,那么使用 Puma 几乎没有什么好处。多个单线程服务器进程会更好。内存消耗会更高,但你会保持理智。编写正确的多线程代码非常困难,甚至专家也会出错。如果您的业务成功、工作保障等都依赖于多线程代码的正常运行和正常运行,那么您将给自己带来很多不必要的痛苦和精神痛苦。

除此之外,让我试着解开你的问题中提出的一些问题。有很多话要说,很难知道从哪里开始。在坐下来阅读这篇论文之前,您可能想给自己倒一杯冷饮或热饮:

当您谈论编写“线程安全”代码时,您需要清楚您的意思。在大多数情况下,“线程安全”代码是指不会以可能导致数据损坏的方式同时修改可变数据的代码。(真是满嘴!)这可能意味着代码根本不允许并发修改可变数据(使用锁),或者它确实允许并发修改,但确保它不会破坏数据(可能使用原子操作和一点黑魔法)。

请注意,当您的线程只读取数据而不是修改数据,或者在使用共享无状态对象时,不存在“线程安全”问题。

“线程安全”的另一个定义可能更适用于您的情况,它与影响外部世界的操作(基本上是 I/O)有关。您可能希望某些操作只发生一次,或者以特定顺序发生。如果执行这些操作的代码在多个线程上运行,它们可能会发生比预期更多的次数,或者以与预期不同的顺序发生,除非您采取措施防止这种情况发生。

看来您的方法仅在首次加载__setup时才被调用。ourgem.rb据我所知,即使多个线程require同时处理同一个文件,MRI 也只会让单个线程加载文件。不知道JRuby是不是也一样。但无论如何,如果你的源文件被多次加载,那就是更深层次问题的征兆。它们应该只在一个线程上加载一次。如果您的应用程序处理多个线程上的请求,则这些线程应该在应用程序加载启动,而不是之前。这是唯一理智的做事方式。

假设一切正常,ourgem.rb将使用单个线程加载。这意味着__setup只会被单个线程调用。在这种情况下,根本不需要担心线程安全问题(就“客户端缓存”的初始化而言)。

即使__setup要由多个线程同时调用,您的原子代码也不会按照您的想法执行。首先,您使用Atomic.new({}).value. 这会将哈希包装在原子引用中,然后将其解包,这样您就可以取回哈希。这是一个无操作。你可以改写{}

其次,您的Atomic#update调用不会阻止初始化代码多次运行。要理解这一点,您需要知道Atomic实际做了什么。

让我拿出旧的、累赘的“增加一个共享计数器”的例子。想象一下以下代码在 2 个线程上运行:

 i += 1

我们都知道这里会出什么问题。您最终可能会遇到以下事件序列:

  1. 线程 A 读取i并递增它。
  2. 线程 B 读取i并递增它。
  3. 线程 A 将其增加的值写回i.
  4. 线程 B 将其增加的值写回i.

所以我们失去了更新,对吧?但是如果我们将计数器值存储在原子引用中并使用Atomic#update呢?然后它会是这样的:

  1. 线程 A 读取i并递增它。
  2. 线程 B 读取i并递增它。
  3. 线程 A 尝试将其增加的值写回i,并成功。
  4. 线程 B 尝试将其增加的值写回i,但失败了,因为该值已经更改。
  5. 线程 Bi再次读取并递增它。
  6. 线程 B 尝试再次将其增加的值写回i,并且这次成功。

你明白吗?Atomic 永远不会停止 2 个线程同时运行相同的代码。它所做的是强制一些线程#update在必要时重试该块,以避免丢失更新。

如果您的目标是确保您的初始化代码只运行一次,那么使用Atomic是一个非常不合适的选择。如果有的话,它可以使它运行更多次,而不是更少(由于重试)。

所以,就是这样。但是如果你还在我这里,我实际上更关心你的“客户”对象本身是否是线程安全的。他们有任何可变状态吗?由于您正在缓存它们,因此初始化它们似乎一定很慢。尽管如此,如果您使用锁使它们成为线程安全的,您可能不会从缓存和线程之间共享它们中获得任何收益。您的“多线程”服务器可能会被简化为实际上是不必要的复杂的单线程服务器。

如果客户端对象没有可变状态,对您有好处。您可以“自由自在”并在线程之间毫无问题地共享它们。如果它们确实具有可变状态,但初始化它们很慢,那么我建议每个线程缓存一个对象,因此它们永远不会共享。Thread[]你的朋友在那儿吗?

于 2013-10-23T11:06:50.423 回答