Ruby WeakRef 有隐式竞争条件?

Ruby WeakRef has implicit race condition?

我正在查看 Ruby WeakRef,似乎 API 的编写方式具有隐含的竞争条件,尽管它似乎不太可能命中。

API 隐含的基本用法是:

obj = Object.new
foo = WeakRef.new(obj)

# Later on:
if (foo.weakref_alive?)
    puts "I can allegedly use #{foo.to_s} now"
end

# Or even:
obj2 = foo.__getobj__ if foo.weakref_alive?

问题在于我们无法控制何时可能发生垃圾收集,例如,考虑另一个定期调用 GC.start.[=17= 的线程 运行 ]

如果我们在 weakref_alive? 检查和使用对象之间发生垃圾回收,那么我们最终会遇到 RefError 异常。

(我实际上希望任何使用 weakref 的大型应用程序——尤其是那些多线程的应用程序——因此偶尔会遇到 RefErrors)

我很惊讶如果对象在我们检查时可用,就没有办法以原子方式安全地获取对象。

所以首先问题是,我是不是多虑了?如果我们在检查对象后立即获取对象(如第二个示例),是否有某种原因我们不必 ever 担心 GC 的发生?如果不是,那么这给了我们第二个问题,即安全使用 weakrefs 的最佳方式。

现在我在 class 中添加了一个 'obj' 方法作为处理它的一种方法:

require 'weakref'
class WeakRef
  def obj
    begin
      return self.__getobj__
    rescue RefError
      return nil
    end
  end
end

但是不必要的 'rescue' 语句有点让我烦恼。我想我们也可以:

require 'weakref'
class WeakRef
  def obj
    savegc = GC.disable
    obj = self.weakref_alive? ? self.__getobj__ : nil
    GC.enable if savegc
    return obj
  end
end

但我怀疑仅禁用并重新启用垃圾收集是否成本低,更不用说这是否是一个完全原子的操作。

ruby GC 专家有什么建议吗?

首先,请注意 WeakRef 对象的预期用途,即代替原始对象。在这里,WeakRef 对象通过转发发送给它的所有消息来实现引用对象的完整鸭子类型接口。因此,WeakRef 对象旨在直接代替原始对象(如果它仍然可用)。

虽然您可能会通过 WeakRef#__getobj__ 获得对原始对象的引用(如果它仍然可用),但这是一个特殊的用例,更多的是消息委托的实现细节。但是,如果您这样做,您可以使用 WeakRef#weakref_alive? 检查引用的对象是否仍然可用。正如您所注意到的,根据您使用的 Ruby 实现,有一个(至少理论上的)竞争条件选项。

为了确保您能够优雅地处理此类竞争条件,您确实可以在 RefError 发生时挽救它。您可以稍微优化一下非竞争条件的情况:

obj = Object.new
foo = WeakRef.new(obj)

begin
  obj2 = foo.__getobj__ if foo.weakref_alive?
rescue RefError
   obj2 = nil
end

您可以对发送到您的弱引用的任何其他消息使用相同的模式(然后将 forwarrdr 发送到您引用的对象),例如

begin
  foo.to_s if foo.weakref_alive?
rescue RefError
  # # do nothing as foo is a dangling reference to a garbage-collected object
end

根据您的用例,这可能有点尴尬。此外,有时需要实际的对象引用而不是包装对象(当查询其特定 class 时,它可能表现不同,例如在 case 语句中)。

这里,一个选项是使用 ObjectSpace::WeakMap 而不是 WeakRef。这个 class 被 WeakRef 内部使用来实际保存弱引用。 Ruby 实际上不鼓励使用此 class 并将其视为内部 class。但是,我发现实现比 WeakRef 更直接的查找更有用。请注意,此区域的行为可能会发生细微变化,在更新 Ruby 版本时阅读变更日志可能是个好主意。

除此之外,使用 ObjectSpace::WeakMap 的示例查找可能如下所示:

# The WeakMap object which can store multiple maps from an
# existing object to another (potentially garbage-collected) object.
# If you need multiple weak references, you can still use
# a single map.
WEAK_MAP = ObjectSpace::WeakMap.new

# Our referenced object which may or may not be garbage-collected later
obj = Object.new

# The "marker" object is the key in map. It is used to look the reference
# to the intended object. You need to always use the same object here
# (rather than e.g. a similar string) as the actual object_id of the marker
# is used for the lookup of the referenced object
marker = Object.new

# Store a reference in the weak map
WEAK_MAP[marker] = obj

#########################################################
# Now do something else...                              #
# obj may be garbage-collected in the meantime.         #
# You need to hold onto the marker object though!       #
#########################################################

# Now, you can retrieve a reference to the actual
# original object (if it is still available) or nil
# if obj was already garbage-collected
obj2 = WEAK_MAP[marker]

如上所述,WeakRef class 在内部使用了这种机制。这里,WeakRef 对象使用自己作为标记。也就是说,只要您持有实际的 WeakRef 对象。 WeakRef#__getobj__ 中的简化查找如下所示:

class WeakRef
  WEAK_MAP = ObjectSpace::WeakMap.new

  def __getobj__
    WEAK_MAP[self] || raise RefError, "Invalid Reference"
  end

  def weakref_alive?
    !WEAK_MAP[self].nil?
    # actually, it's this mostly equivalent code
    # WEAK_MAP.key?(self)
  end
end

您可以在 https://github.com/ruby/ruby/blob/master/lib/weakref.rb 找到 WeakRef class 的实现 - 看一看,它实际上非常可读。