【问题标题】:How to implement atomic reference counter that does not overflow?如何实现不溢出的原子引用计数器?
【发布时间】:2016-06-28 13:53:56
【问题描述】:

我正在考虑基于原子整数的引用计数,这样可以避免溢出。怎么做?

请不要关注这种溢出是否是一个现实问题。这项任务本身引起了我的兴趣,即使实际上并不重要。


示例

引用计数的示例实现在Boost.Atomic 中显示为示例。基于该示例,我们可以提取以下示例代码:

struct T
{
    boost::atomic<boost::uintmax_t> counter;
};

void add_reference(T* ptr)
{
    ptr->counter.fetch_add(1, boost::memory_order_relaxed);
}

void release_reference(T* ptr)
{
    if (ptr->counter.fetch_sub(1, boost::memory_order_release) == 1) {
        boost::atomic_thread_fence(boost::memory_order_acquire);
        delete ptr;
    }
}

另外给出以下解释

始终可以使用memory_order_relaxed 来增加引用计数器:对对象的新引用只能从现有引用形成,并且将现有引用从一个线程传递到另一个线程必须已经提供了任何所需的同步。

强制在一个线程中(通过现有引用)对对象进行任何可能的访问以在删除另一个线程中的对象之前发生,这一点很重要。这是通过删除引用后的“释放”操作(显然之前必须通过此引用访问对象)和删除对象之前的“获取”操作来实现的。

可以将memory_order_acq_rel 用于fetch_sub 操作,但是当引用计数器尚未达到零时,这会导致不需要的“获取”操作,并且可能会造成性能损失。

编辑 >>>

似乎Boost.Atomic 文档在这里可能是错误的。毕竟可能需要acq_rel

当使用std::atomic 完成时,至少是boost::shared_ptr 的实现(还有其他实现)。见文件boost/smart_ptr/detail/sp_counted_base_std_atomic.hpp

Herb Sutter 在他的讲座 C++ and Beyond 2012: Herb Sutter - atomic<> Weapons, 2 of 2 中也提到了这一点(引用计数部分从 1:19:51 开始)。此外,他似乎不鼓励在这次谈话中使用栅栏。

感谢user 2501 在下面的 cmets 中指出这一点。


初步尝试

现在的问题是 add_reference 写的可能(在某些时候)溢出。它会默默地这样做。这显然会在调用匹配的release_reference 时导致问题,这会过早地破坏对象。 (前提是add_reference 会被再次调用以到达1。)

我在考虑如何让add_reference 检测溢出并优雅地失败而不冒任何风险。

0 相比,一旦我们离开fetch_add 就不会这样做,因为在两者之间,其他一些线程可能会再次调用add_reference(到达1)然后release_reference(错误地破坏有效的对象)。

先检查(使用load)也无济于事。这样,其他线程可以在我们对loadfetch_add 的调用之间添加自己的引用。


这是解决方案吗?

然后我想也许我们可以从 load 开始,但前提是我们这样做 compare_exchange

所以首先我们执行load 并获得一个本地值。如果是std::numeric_limits&lt;boost::uintmax_t&gt;::max(),那么我们就失败了。 add_reference 无法添加其他引用,因为所有可能的引用都已被占用。

否则我们将创建另一个本地值,即先前的本地引用计数加上1

现在我们 compare_exchange 提供原始本地引用计数作为预期值(这确保同时没有其他线程修改引用计数),并将增加的本地引用计数作为所需值。

由于compare_exchange 可能失败,我们必须在循环中执行此操作(包括load)。直到成功或检测到最大值。


一些问题

  1. 这样的解决方案正确吗?
  2. 需要什么样的内存排序才能使其有效?
  3. 应该使用哪个compare_exchange_weak_strong?
  4. 会影响release_reference 功能吗?
  5. 在实践中使用了吗?

【问题讨论】:

  • fetch_add 返回原子的先前值。这还不足以检测溢出吗?
  • nit - uint 不会溢出,它们会换行,可以检测到。
  • 如果事后检测到溢出,您也可以abort,不是吗?无论哪种方式,您都可能无法安全地继续执行程序。
  • Herb Sutter 提到你需要在大约 1 小时 22 分钟时为减量提供获取-释放语义:channel9.msdn.com/Shows/Going+Deep/…
  • @AdamBadura 放松增量,acq_rel 减量。最重要的是,如果您不确定,请使用默认模式(您不必显式编写),即memory_order_seq_cst。否则你只会打自己的脚。

标签: c++ multithreading atomic integer-overflow reference-counting


【解决方案1】:

解决方案是正确的,也许可以通过一件事来改进。目前,如果该值在本地 CPU 中达到最大值,则可以由另一个 CPU 减小,但当前 CPU 仍会缓存旧值。值得用相同的expectednewValue 做虚拟compare_exchange 以确认最大值仍然存在,然后才抛出异常(或任何你想要的)。

剩下的:

无论您使用_weak 还是_strong 都没有关系,因为它无论如何都会循环运行,因此下一个load 将非常可靠地获得最新值。

对于add_referencerelease_reference - 谁来检查它是否真的被添加了?会不会抛出异常。如果是,它可能会起作用。但通常最好让这样低级别的事情不会失败,而是使用 uintptr_t 作为引用计数器,这样它就永远不会溢出,因为它大到足以覆盖地址空间,因此可以同时存在任意数量的对象。

不,由于上述原因,它没有在实践中使用。

【讨论】:

  • 但是这些操作所需的内存排序标志怎么样?
  • 我也没有收到关于“dummy compare_exchange”的评论。我什么时候应该这样做?在load 之后?这与另一个load 有何不同?
  • @AdamBadura : compare_exchange 将确保您期望的最大值仍然存在,并将强制与其他处理器(或全局内存总线)同步。如果失败,则意味着您可以重复 load-inc-cmpxchg 循环。因为订购发布会更有意义,但通常它们都可以工作。
【解决方案2】:

快速数学:假设 uint 是 32 位,所以最大 uint 是 4G(40 亿左右)。每个引用/指针至少有 4 个字节(如果您在 64 位系统上,则为 8 个字节),因此要溢出,您需要 16GB 的内存专门用于存储指向同一对象的引用,这应该指向一个严重的设计缺陷。

我会说这在今天不是问题,在可预见的将来也不是。

【讨论】:

  • 我明确要求避免像“这不是一个现实的问题”这样的答案。这个特定的用例可能不现实,但它仍然是一个可以解决的问题。虽然提到众所周知的事情很容易理解。它的解决方案在需要溢出检查的其他用例中可能很有用。特别是如果您认为您可能有一些更小的任意限制,而不是最大可能值。
【解决方案3】:

这个问题没有实际意义。即使假设原子增量需要 1 个 CPU 周期(事实并非如此!),在 4GHz CPU 上,它也需要半年时间来环绕 64 位整数,前提是 CPU 除了继续递增之外什么都不做。

考虑到实际程序的实际情况,我很难相信这是一个真正困扰您的问题。

【讨论】:

  • 我明确要求避免像“这不是一个现实的问题”这样的答案。这个特定的用例可能不现实,但它仍然是一个可以解决的问题。虽然提到众所周知的事情很容易理解。它的解决方案在需要溢出检查的其他用例中可能很有用。特别是如果您认为您可能有一些更小的任意限制,而不是最大可能值。
  • 我做到了!最后,这是一个关于如何可靠地检测(和防止)原子整数溢出/环绕的问题。当我想到它时,我用引用计数“主题”来问它。我可以删除“主题”吗?是的,我可以,甚至应该。但这个“主题”是否如此令人不安以至于无法回答这个问题?
猜你喜欢
  • 1970-01-01
  • 2013-01-05
  • 2015-09-24
  • 1970-01-01
  • 2015-11-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-10
  • 2016-03-14
相关资源
最近更新 更多